我们先来分析以下 一个封闭控制得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)
层级越高 越显示在最上面
代码很少 很简单
首先先声明权限
<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得做法
- 第一步 我们先弄一个假的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>
- 取消默认得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;
}
基本上有了以上得权限 就可以为所欲为了 本博客只是为了提供方便 做了一个寻找收藏得这么个过程 如果有别的需求什么得 可以大家一起讨论讨论