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

我们先来分析以下 一个封闭控制得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;
    }

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • 项目需求,要求本公司自己的外发设备必须只能用指定授权的白名单的应用。一开始,我是有抵触的,认为是黑科技。没办法,工...
    爱码士平头哥阅读 7,736评论 0 0
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,825评论 25 707
  • M家毛衣 简陌很早就知道盛木果是个不会整理东西的人。 而这点也刚刚好被他利用了,前段时间整理木果宿舍...
    阆缘阅读 241评论 0 0
  • 在你和她潇洒同学聚会,踏山游水,欢歌笑语的这几天里,我过的异常痛苦和艰辛,你们的影子不断的出现在我脑海中,挥之不去...
    summerimage阅读 275评论 0 0