美文网首页功能专区Android开发Android技术知识
实现一个封闭控制得launcher所需得权限与控制

实现一个封闭控制得launcher所需得权限与控制

作者: 超威蓝猫l | 来源:发表于2018-03-16 16:01 被阅读95次

    我们先来分析以下 一个封闭控制得launcher需要实现什么功能

    • 不能有消息弹出 (notification)
    • 通知栏不允许下拉
    • 长按home键 以及home键不能退出应用
    • 拿到目前运行得app
    • 开放未知应用得安装
    • 管理员权限
    • etc

    本博客只提供思路 测试手机为原生5.1 至于碎片化需要自己挖坑

    不能有消息弹出(notification)

    关键字:Notification access(通知使用权)
    类:NotificationListenerService

    NotificationListenerService于Android4.3引入 拥有此权限后可以读取以及监听App中得通知栏notification。

    首先需要新建你的service 并继承NotificationListenerService

    public class MyNotificationListenerService extends NotificationListenerService {
        public MyNotificationListenerService() {
        }
    
    
        @Override
        public void onCreate() {
            super.onCreate();
        }
    
        @Override
        public IBinder onBind(Intent intent) {
            return super.onBind(intent);//这个地方也很重要  少了这个不跑
        }
    
        @Override
        public void onNotificationRemoved(StatusBarNotification sbn) {
            //有消息被移除可以监听这里
        }
    
        @Override
        public void onNotificationPosted(StatusBarNotification sbn) {
            //以下可以按照需要禁止部分notification 甚至一刀切
            int id = sbn.getId();
            NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            notificationManager.cancel(id);
        }
    
        @Override
        public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
            super.onNotificationPosted(sbn, rankingMap);
        }
    
        @Override
        public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
            super.onNotificationRemoved(sbn, rankingMap);
        }
    
        @Override
        public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, int reason) {
            super.onNotificationRemoved(sbn, rankingMap, reason);
        }
    
        @Override
        public void onListenerConnected() {
            super.onListenerConnected();
        }
    
        @Override
        public void onListenerDisconnected() {
            super.onListenerDisconnected();
        }
    
    }
    

    其次 还需要在AndroidManifest.xml中声明 权限和intent-filter

    <service
               android:name=".MyNotificationListenerService"
               android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
               <intent-filter>
                   <action android:name="android.service.notification.NotificationListenerService" />
               </intent-filter>
           </service>
    

    除了如上 还有一些有用的方法

    StatusBarNotification[] sbns = getActiveNotifications(); //返回所有通知
    cancelAllNotifications();//清楚所有通知
    ...
    

    点进去cancelAllNotifications方法 可以看到
    必须等onListenerConnected回调了之后才能调用这个方法

    <p>The service should wait for the {@link #onListenerConnected()} event
         * before performing this operation.
    

    以上步骤完成之后,还需要申请权限

    try {
                        Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        startActivity(intent);
                    } catch (ActivityNotFoundException e) {
                        try {
                            Intent intent = new Intent();
                            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                            ComponentName cn = new ComponentName("com.android.settings", "com.android.settings.Settings$NotificationAccessSettingsActivity");
                            intent.setComponent(cn);
                            intent.putExtra(":settings:show_fragment", "NotificationAccessSettings");
                            startActivity(intent);
                        } catch (Exception ex) {
                            ex.printStackTrace();
                        }
                    }
    

    以下是判断有没有权限

    
            boolean enable = false;
            String packageName = mContext.getApplicationContext().getPackageName();
            String flat = Settings.Secure.getString(mContext.getContentResolver(), "enabled_notification_listeners");
            if (flat != null) {
                enable = flat.contains(packageName);
            }
            return enable;
    
    

    使用以上代码 并获得权限后 你可能会发现有时候会失效 这时候你要让系统杀了service重来 需要注意

    PackageManager pm = context.getPackageManager();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                pm.setComponentEnabledSetting(new ComponentName(context, MyNotificationListenerService.class),
                        PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
            }
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                pm.setComponentEnabledSetting(new ComponentName(context, MyNotificationListenerService.class),
                        PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
            }
    

    通知栏不能下拉

    这里我们使用一个取巧的东西 WindowManager 当然在原生上不需要权限就能使用系统权限 如果是国产rom(小米)什么的还得单独得去授权悬浮窗权限,这里我们不做介绍

    windowManager(WindowManagerService)是Framework层得窗口管理服务。
    在andoroid 官网上 第一句话就是The interface that apps use to talk to the window manager.(app跟windowmanger交流得接口)

    window分为三个层级 应用window(1-99) 子window(1000-1999) 系统window(2000-2999)
    层级越高 越显示在最上面

    20150820144141500.jpg

    代码很少 很简单
    首先先声明权限
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

    代码如下:

    WindowManager manager = ((WindowManager) getApplicationContext()
                    .getSystemService(Context.WINDOW_SERVICE));
    
            WindowManager.LayoutParams localLayoutParams = new WindowManager.LayoutParams();
            localLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
            localLayoutParams.gravity = Gravity.TOP;
            localLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
                    WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
    
            localLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
            localLayoutParams.height = getNotifyBarHeight(this);
            localLayoutParams.format = PixelFormat.TRANSPARENT;
    
            View view = new View(this);
    
            manager.addView(view, localLayoutParams);
    
    //获取status bar高度
    public static int getNotifyBarHeight(Context context) {
            int result = 0;
            int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
            if (resourceId > 0) {
                result = context.getResources().getDimensionPixelSize(resourceId);
            }
            return result;
        }
    

    在代码中 添加了一个空白得view 此时 界面看上去无二 但是实际上已经不能下拉了。

    这里我们注意 我们用得type是LayoutParams.TYPE_SYSTEM_ERROR,而不是平时用得
    LayoutParams.TYPE_SYSTEM_ALERT
    官方解释:内部系统错误窗口 显示在任何东西得最顶层。在多用户系统中,只显示在自己用户得窗口上

     /**
             * Window type: internal system error windows, appear on top of
             * everything they can.
             * In multiuser systems shows only on the owning user's window.
             * @deprecated for non-system apps. Use {@link #TYPE_APPLICATION_OVERLAY} instead.
             */
    @Deprecated
            public static final int TYPE_SYSTEM_ERROR       = FIRST_SYSTEM_WINDOW+10;
    

    现在可以显示在上面了 除此之外还需要屏蔽下拉手势
    我们使用三个flag
    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
    WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
    重点看第三个得解释

    /** Window flag: this window won't ever get key input focus, so the
             * user can not send key or other button events to it.  Those will
             * instead go to whatever focusable window is behind it.  This flag
             * will also enable {@link #FLAG_NOT_TOUCH_MODAL} whether or not that
             * is explicitly set.
             *
             * <p>Setting this flag also implies that the window will not need to
             * interact with
             * a soft input method, so it will be Z-ordered and positioned
             * independently of any active input method (typically this means it
             * gets Z-ordered on top of the input method, so it can use the full
             * screen for its content and cover the input method if needed.  You
             * can use {@link #FLAG_ALT_FOCUSABLE_IM} to modify this behavior. */
            public static final int FLAG_NOT_FOCUSABLE      = 0x00000008;
    
    
    /** Window flag: place the window within the entire screen, ignoring
             *  decorations around the border (such as the status bar).  The
             *  window must correctly position its contents to take the screen
             *  decoration into account.  This flag is normally set for you
             *  by Window as described in {@link Window#setFlags}. */
            public static final int FLAG_LAYOUT_IN_SCREEN   = 0x00000100;
    
    
    //看这句 如果没有设置FLAG_NOT_FOCUSABLE,那么点击事件可以穿透window 反之所有点击事件都会留在这个窗口(我们设置了这个)
    /** Window flag: even when this window is focusable (its
             * {@link #FLAG_NOT_FOCUSABLE} is not set), allow any pointer events
             * outside of the window to be sent to the windows behind it.  Otherwise
             * it will consume all pointer events itself, regardless of whether they
             * are inside of the window. */
            public static final int FLAG_NOT_TOUCH_MODAL    = 0x00000020;
    

    home键不能退出应用

    一般home键就是回桌面得,所以我们需要将自己得launcher设置为defalut

    要让应用成为lancher 设置以下intent filter就行

    <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.HOME" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
    

    在我们装上自己得app得时候 我们得launcher肯定不是default得 我们现在需要取消掉默认得launcher先
    这做法是一种work around得做法

    1. 第一步 我们先弄一个假的activity 界面什么得都不需要
    <activity
                android:name=".FakeLauncherActivity"
                android:enabled="false">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.HOME" />
                    <category android:name="android.intent.category.DEFAULT" />
                </intent-filter>
            </activity>
    
    1. 取消默认得activity
    //先调用这个
    public static void resetPreferredLauncherAndOpenChooser(Context context) {
            PackageManager packageManager = context.getPackageManager();
            ComponentName componentName = new ComponentName(context, FakeLauncherActivity.class);
            packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
    
            Intent selector = new Intent(Intent.ACTION_MAIN);
            selector.addCategory(Intent.CATEGORY_HOME);
            selector.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(selector);
    
            packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, PackageManager.DONT_KILL_APP);
        }
    //后调用这个
    private void launchAppChooser() {
            Intent intent = new Intent(Intent.ACTION_MAIN);
            intent.addCategory(Intent.CATEGORY_HOME);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            startActivity(intent);
        }
    

    这时候 提示用户设置自己得app为always

    2.判断自己是否是default launcher
    这段代码有瑕疵 设置过一次后貌似会经常为true 但是目前先用这个

    public boolean isDefaultLauncherAccess() {
            IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN);
            filter.addCategory(Intent.CATEGORY_HOME);
    
            List<IntentFilter> filters = new ArrayList<IntentFilter>();
            filters.add(filter);
    
            String myPackageName = mContext.getApplicationContext().getPackageName();
            List<ComponentName> activities = new ArrayList<ComponentName>();
            PackageManager packageManager = (PackageManager) mContext.getApplicationContext().getPackageManager();
    
            packageManager.getPreferredActivities(filters, activities, null);
    
            for (ComponentName activity : activities) {
                if (myPackageName.equals(activity.getPackageName())) {
                    return true;
                }
            }
            return false;
        }
    

    拿到目前运行得app

    在4.4以下 有另外得方法可以获取 这里描述5.0
    我们需要 Usage access得权限
    废话少说 直接上代码

    所需权限

    //5.0
    <uses-permission
            android:name="android.permission.PACKAGE_USAGE_STATS"
            tools:ignore="ProtectedPermissions" />
    //4.4
        <uses-permission android:name="android.permission.GET_TASKS" />
    

    判断是否有权限

    public boolean isUsageAccess() {
            long time = System.currentTimeMillis();
            @SuppressLint("WrongConstant") UsageStatsManager usageStatsManager = (UsageStatsManager) mContext.getApplicationContext().getSystemService("usagestats");
            List<UsageStats> queryUsageStats = usageStatsManager.queryUsageStats(
                    UsageStatsManager.INTERVAL_BEST, 0, time);
            if (queryUsageStats == null || queryUsageStats.isEmpty()) {
                return false;
            }
            return true;
        }
    

    去获得权限

    Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
                        startActivity(intent);
    

    屏蔽(规避)长按home键

    有了上面判断运行app得权限 我们可以来做这个了

    在点击home键得时候 系统都会发出ACTION_CLOSE_SYSTEM_DIALOGS得广播 这个广播是用来关闭系统dialog得

    所以我们来监听这个

    public class KeyReceiver extends BroadcastReceiver {
    
        private static final String LOG_TAG = "HomeReceiver";
        private static final String SYSTEM_DIALOG_REASON_KEY = "reason";
        private static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps";
        private static final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
        private static final String SYSTEM_DIALOG_REASON_LOCK = "lock";
        private static final String SYSTEM_DIALOG_REASON_ASSIST = "assist";
    
    
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) {
                String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);
                if (reason == null) {
                    return;
                }
                if (reason.equalsIgnoreCase("globalactions")) {
                    //屏蔽电源长按键的方法:
                    Intent myIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
                    myIntent.putExtra("myReason", true);
                    context.sendOrderedBroadcast(myIntent, null);
    
                } else if (SYSTEM_DIALOG_REASON_HOME_KEY.equals(reason)) {
                    // 短按Home键
                } else if (SYSTEM_DIALOG_REASON_RECENT_APPS.equals(reason)) {
    
    
                    // 长按Home键 或者 activity切换键
                    Intent intent1 = new Intent(context, InterceptActivity.class);
                    intent1.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    context.startActivity(intent1);
    
    
                    Activity activity = ManagerActivityUtils.getInstance().topActivity();
                    if (activity == null) {
                        return;
                    }
                    final ActivityManager activityManager = (ActivityManager) ManagerActivityUtils.getInstance().topActivity().getApplicationContext()
                            .getSystemService(Context.ACTIVITY_SERVICE);
    
                    activityManager.moveTaskToFront(ManagerActivityUtils.getInstance().topActivity().getTaskId(), 0);
    
                    new Handler(context.getMainLooper()).postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            activityManager.moveTaskToFront(ManagerActivityUtils.getInstance().topActivity().getTaskId(), 0);
                        }
                    }, 10);
    
                    new Handler(context.getMainLooper()).postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            activityManager.moveTaskToFront(ManagerActivityUtils.getInstance().topActivity().getTaskId(), 0);
                        }
                    }, 20);
    
                }
            }
        }
    }
    

    这里我们只要看SYSTEM_DIALOG_REASON_RECENT_APPS 这个elseif

    我们来重点关注这个moveTaskToFront 这个是强制将栈移到前台
    将栈移到前台我们就能覆盖最近任务栏这个界面

    至于ManagerActivityUtils.getInstance().topActivity();这个方法 是拿到自己最顶层得acitivty(每启动一个actiivty就存进去list,ondestroy就remove) 因为只有activity能拿到TaskId

    注意:::这个recevier只能动态注册 否则听不到

    开放未知应用

    startActivity(new Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS));
    
    public boolean isUnknownInstallAccess() {
            boolean isNonPlayAppAllowed = false;
            try {
                isNonPlayAppAllowed = Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.INSTALL_NON_MARKET_APPS) == 1;
            } catch (Settings.SettingNotFoundException e) {
                e.printStackTrace();
            }
            return isNonPlayAppAllowed;
        }
    

    获得管理员权限

    拿到管理员权限我们就能远程锁屏 甚至禁用摄像头

    首先我们新建recevier 并继承DeviceAdminReceiver

    public class AdminReceiver extends DeviceAdminReceiver {
    
        @Override
        public void onEnabled(Context context, Intent intent) {
    
        }
    
        @Override
        public void onDisabled(Context context, Intent intent) {
    //可以上网搜索一下 这个地方可以做流氓应用 不让用户取消 思路是进来就黑屏 让等待时间过去
        }
    
        @Override
        public CharSequence onDisableRequested(Context context, Intent intent) {
    //这个地方 想要取消得时候会显示 你return得文字
    
            return context.getString(R.string.str_onDisableRequested_hint);
        }
    
        @Override
        public void onPasswordChanged(Context context, Intent intent) {
    
        }
    
        @Override
        public void onPasswordFailed(Context context, Intent intent) {
    
        }
    
        @Override
        public void onPasswordSucceeded(Context context, Intent intent) {
    
        }
    }
    

    Manifest 声明

    <receiver android:name=".receiver.AdminReceiver">
                <meta-data
                    android:name="android.app.device_admin"
                    android:resource="@xml/admin" />
    
                <intent-filter>
                    <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
                </intent-filter>
            </receiver>
    

    这里有一个resource 里面是所要申请得权限

    在res文件夹里新建一个xml文件夹
    以下是去官网拷贝得 网上得一些都不全

    <?xml version="1.0" encoding="utf-8"?>
    <device-admin>
        <uses-policies>
            <limit-password />
            <watch-login />
            <reset-password />
            <force-lock />
            <wipe-data />
            <expire-password />
            <encrypted-storage />
            <disable-camera />
            <disable-keyguard-features />
        </uses-policies>
    </device-admin>
    

    如上都做好后 就可以申请了

    ComponentName mDeviceAdminSample = new ComponentName(this, AdminReceiver.class);
                    Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
                    intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, mDeviceAdminSample);
                    intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION, getString(R.string.str_admin_explain));
                    startActivity(intent);
    

    判断是否是管理员

    @Override
        public boolean isAdminAccess() {
            DevicePolicyManager manager = (DevicePolicyManager) mContext.getApplicationContext().getSystemService(Context.DEVICE_POLICY_SERVICE);
            boolean adminActive = manager.isAdminActive(new ComponentName(mContext, AdminReceiver.class));
            return adminActive;
        }
    

    基本上有了以上得权限 就可以为所欲为了 本博客只是为了提供方便 做了一个寻找收藏得这么个过程 如果有别的需求什么得 可以大家一起讨论讨论

    相关文章

      网友评论

      本文标题:实现一个封闭控制得launcher所需得权限与控制

      本文链接:https://www.haomeiwen.com/subject/btalqftx.html