需求场景:如果你在为很多的app提供SDK服务,那么你可能没有自己的Activity,但你需要检测当前的App是否处于前台,方便做一些特殊的逻辑。
需求分析:
在android平台你有很多的方式来解决这个问题,例如你可以使用ActivityManager.getRunningTasks获取当前运行的task列表,并从最上层的task的topActivity属性获得当前前台的activity名称(ComponentName),从而获取包名。然后对比当前应用的包名,从而判断当前应用是否处于前台。但该方法在android 5.0及以上版本被移除了,官方解释如下:
This method was deprecated in API level 21.
As ofLOLLIPOP
, this method is no longer available to third party applications: the introduction of document-centric recents means it can leak person information to the caller. For backwards compatibility, it will still return a small subset of its data: at least the caller’s own tasks, and possibly some other tasks such as home that are known to not be sensitive.
翻译:
该方法在 API level 21 已经被废弃
在LOLLIPOP
, 该方法不再对第三方应用开放。引用文档中心的最新信息意思是该方法会将app的隐私信息泄露给调用方。为了向前兼容,它仍然会返回数据中的其中一小部分: 至少包含调用方自己的task任务,并且也会有其它已知的不敏感的task列表,例如home桌面任务
那么也就是说我们需要另外找方法来解决了。
可选方案对比:
六种方法的区别
方法 | 判断原理 | 需要权限 | 可以判断其他应用位于前台 | 特点 |
---|---|---|---|---|
方法一 | RunningTask | 否 | Android4.0系列可以,5.0以上机器不行 | 5.0此方法被废弃 |
方法二 | RunningProcess | 否 | 当App存在后台常驻的Service时失效 | 无 |
方法三 | ActivityLifecycleCallbacks | 否 | 否 | 简单有效,代码最少 |
方法四 | UsageStatsManager | 是 | 是 | 需要用户手动授权 |
方法五 | 通过Android无障碍功能实现 | 否 | 是 | 需要用户手动授权 |
方法六 | 读取/proc目录下的信息 | 否 | 是 | 当proc目录下文件夹过多时,过多的IO操作会引起耗时 |
方法一:通过RunningTask
原理
当一个App处于前台的时候,会处于RunningTask的这个栈的栈顶,所以我们可以取出RunningTask的栈顶的任务进程,看他与我们的想要判断的App的包名是否相同,来达到效果
缺点
getRunningTask方法在Android5.0以上已经被废弃,只会返回自己和系统的一些不敏感的task,不再返回其他应用的task,用此方法来判断自身App是否处于后台,仍然是有效的,但是无法判断其他应用是否位于前台,因为不再能获取信息
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 |
/** * 方法1:通过getRunningTasks判断App是否位于前台,此方法在5.0以上失效 * * @param context 上下文参数 * @param packageName 需要检查是否位于栈顶的App的包名 * @return */ public static boolean getRunningTask(Context context, String packageName) { ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); ComponentName cn = am.getRunningTasks(1).get(0).topActivity; return !TextUtils.isEmpty(packageName) && packageName.equals(cn.getPackageName()); } |
方法二:通过RunningProcess
原理
通过runningProcess获取到一个当前正在运行的进程的List,我们遍历这个List中的每一个进程,判断这个进程的一个importance 属性是否是前台进程,并且包名是否与我们判断的APP的包名一样,如果这两个条件都符合,那么这个App就处于前台
缺点:
在聊天类型的App中,常常需要常驻后台来不间断的获取服务器的消息,这就需要我们把Service设置成START_STICKY,kill 后会被重启(等待5秒左右)来保证Service常驻后台。如果Service设置了这个属性,这个App的进程就会被判断是前台,代码上的表现就是appProcess.importance的值永远是 ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND,这样就永远无法判断出到底哪个是前台了。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/** * 方法2:通过getRunningAppProcesses的IMPORTANCE_FOREGROUND属性判断是否位于前台,当service需要常驻后台时候,此方法失效, * 在小米 Note上此方法无效,在Nexus上正常 * * @param context 上下文参数 * @param packageName 需要检查是否位于栈顶的App的包名 * @return */ public static boolean getRunningAppProcesses(Context context, String packageName) { ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses(); if (appProcesses == null) { return false; } for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) { if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName.equals(packageName)) { return true; } } return false; } |
方法三:通过ActivityLifecycleCallbacks
原理
AndroidSDK14 (android 4.0) 在Application类里增加了ActivityLifecycleCallbacks,我们可以通过这个Callback拿到App所有Activity的生命周期回调。
1 2 3 4 5 6 7 8 9 |
public interface ActivityLifecycleCallbacks { void onActivityCreated(Activity activity, Bundle savedInstanceState); void onActivityStarted(Activity activity); void onActivityResumed(Activity activity); void onActivityPaused(Activity activity); void onActivityStopped(Activity activity); void onActivitySaveInstanceState(Activity activity, Bundle outState); void onActivityDestroyed(Activity activity); } |
知道这些信息,我们就可以用更官方的办法来解决问题,当然还是利用Activity生命周期的特性,我们只需要在Application的onCreate()里去注册上述接口,然后由Activity回调回来运行状态即可。
可能还有人在纠结,我用back键切到后台和用Home键切到后台,一样吗?以上方法适用吗?在Android应用开发中一般认为back键是可以捕获的,而Home键是不能捕获的(除非修改framework),但是上述方法从Activity生命周期着手解决问题,虽然这两种方式的Activity生命周期并不相同,但是二者都会执行onStop();所以并不关心到底是触发了哪个键切入后台的。另外,Application是否被销毁,都不会影响判断的正确性
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
public class MyApplication extends Application { private int appCount = 0; @Override public void onCreate() { super.onCreate(); registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { } @Override public void onActivityStarted(Activity activity) { appCount++; } @Override public void onActivityResumed(Activity activity) { } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { appCount--; } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override public void onActivityDestroyed(Activity activity) { } }); } public int getAppCount() { return appCount; } public void setAppCount(int appCount) { this.appCount = appCount; } } |
方法四:通过使用UsageStatsManager获取
原理
通过使用UsageStatsManager获取,此方法是Android5.0之后提供的新API,可以获取一个时间段内的应用统计信息,但是必须满足一下要求
使用前提
- 此方法只在android5.0以上有效
- AndroidManifest中加入此权限
1 |
<span class="pl-k"><</span>uses<span class="pl-k">-</span>permission android<span class="pl-k">:</span>name<span class="pl-k">=</span><span class="pl-s"><span class="pl-pds">"</span>android.permission.PACKAGE_USAGE_STATS<span class="pl-pds">"</span></span> <span class="pl-k">/</span><span class="pl-k">></span> |
- 打开手机设置,点击安全-高级,在有权查看使用情况的应用中,为这个App打上勾
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
/** * 方法4:通过使用UsageStatsManager获取,此方法是ndroid5.0A之后提供的API * 必须: * 1. 此方法只在android5.0以上有效 * 2. AndroidManifest中加入此权限<uses-permission xmlns:tools="http://schemas.android.com/tools" android:name="android.permission.PACKAGE_USAGE_STATS" * tools:ignore="ProtectedPermissions" /> * 3. 打开手机设置,点击安全-高级,在有权查看使用情况的应用中,为这个App打上勾 * * @param context 上下文参数 * @param packageName 需要检查是否位于栈顶的App的包名 * @return */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static boolean queryUsageStats(Context context, String packageName) { class RecentUseComparator implements Comparator<UsageStats> { @Override public int compare(UsageStats lhs, UsageStats rhs) { return (lhs.getLastTimeUsed() > rhs.getLastTimeUsed()) ? -1 : (lhs.getLastTimeUsed() == rhs.getLastTimeUsed()) ? 0 : 1; } } RecentUseComparator mRecentComp = new RecentUseComparator(); long ts = System.currentTimeMillis(); UsageStatsManager mUsageStatsManager = (UsageStatsManager) context.getSystemService("usagestats"); List<UsageStats> usageStats = mUsageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_BEST, ts - 1000 * 10, ts); if (usageStats == null || usageStats.size() == 0) { if (HavaPermissionForTest(context) == false) { Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); Toast.makeText(context, "权限不够\n请打开手机设置,点击安全-高级,在有权查看使用情况的应用中,为这个App打上勾", Toast.LENGTH_SHORT).show(); } return false; } Collections.sort(usageStats, mRecentComp); String currentTopPackage = usageStats.get(0).getPackageName(); if (currentTopPackage.equals(packageName)) { return true; } else { return false; } } /** * 判断是否有用权限 * * @param context 上下文参数 */ @TargetApi(Build.VERSION_CODES.KITKAT) private static boolean HavaPermissionForTest(Context context) { try { PackageManager packageManager = context.getPackageManager(); ApplicationInfo applicationInfo = packageManager.getApplicationInfo(context.getPackageName(), 0); AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); int mode = appOpsManager.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, applicationInfo.uid, applicationInfo.packageName); return (mode == AppOpsManager.MODE_ALLOWED); } catch (PackageManager.NameNotFoundException e) { return true; } } |
方法五:通过Android自带的无障碍功能
此方法无法直观的通过下拉通知视图来进行前后台的观察,请到LogCat中进行观察即可,以下是LogCat中打印的信息
原理
Android 辅助功能(AccessibilityService) 为我们提供了一系列的事件回调,帮助我们指示一些用户界面的状态变化。 我们可以派生辅助功能类,进而对不同的 AccessibilityEvent 进行处理。 同样的,这个服务就可以用来判断当前的前台应用
优势
- AccessibilityService 有非常广泛的 ROM 覆盖,特别是非国产手机,从 Android API Level 8(Android 2.2) 到 Android Api Level 23(Android 6.0)
- AccessibilityService 不再需要轮询的判断当前的应用是不是在前台,系统会在窗口状态发生变化的时候主动回调,耗时和资源消耗都极小
- 不需要权限请求
- 它是一个稳定的方法,与 “方法6”读取 /proc 目录不同,它并非利用 Android 一些设计上的漏洞,可以长期使用的可能很大
- 可以用来判断任意应用甚至 Activity, PopupWindow, Dialog 对象是否处于前台
劣势
- 需要要用户开启辅助功能
- 辅助功能会伴随应用被“强行停止”而剥夺
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/** * 方法5:通过Android自带的无障碍功能,监控窗口焦点的变化,进而拿到当前焦点窗口对应的包名 * 必须: * 1. 创建ACCESSIBILITY SERVICE INFO 属性文件 * 2. 注册 DETECTION SERVICE 到 ANDROIDMANIFEST.XML * * @param context * @param packageName * @return */ public static boolean getFromAccessibilityService(Context context, String packageName) { if (DetectService.isAccessibilitySettingsOn(context) == true) { DetectService detectService = DetectService.getInstance(); String foreground = detectService.getForegroundPackage(); Log.d("wenming", "**方法五** 当前窗口焦点对应的包名为: =" + foreground); return packageName.equals(foreground); } else { Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); Toast.makeText(context, R.string.accessbiliityNo, Toast.LENGTH_SHORT).show(); return false; } } |
方法六:读取Linux系统内核保存在/proc目录下的process进程信息
此方法并非我原创,原作者是国外的大神,GitHub项目在这里,也一并加入到工程中,供大家做全面的参考选择
原理
无意中看到乌云上有人提的一个漏洞,Linux系统内核会把process进程信息保存在/proc目录下,Shell命令去获取的他,再根据进程的属性判断是否为前台
优点
- 不需要任何权限
- 可以判断任意一个应用是否在前台,而不局限在自身应用
缺点
- 当/proc下文件夹过多时,此方法是耗时操作
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/** * @return a list of <i>all</i> processes running on the device. */ public static List<AndroidProcess> getRunningProcesses() { List<AndroidProcess> processes = new ArrayList<>(); File[] files = new File("/proc").listFiles(); for (File file : files) { if (file.isDirectory()) { int pid; try { pid = Integer.parseInt(file.getName()); } catch (NumberFormatException e) { continue; } try { processes.add(new AndroidProcess(pid)); } catch (IOException e) { log(e, "Error reading from /proc/%d.", pid); // System apps will not be readable on Android 5.0+ if SELinux is enforcing. // You will need root access or an elevated SELinux context to read all files under /proc. } } } return processes; } /** * @return {@code true} if this process is in the foreground. */ public static boolean isMyProcessInTheForeground() { List<AndroidAppProcess> processes = getRunningAppProcesses(); int myPid = android.os.Process.myPid(); for (AndroidAppProcess process : processes) { if (process.pid == myPid && process.foreground) { return true; } } return false; } |
Greate pieces. Keep posting such kind of information on your
page. Im really impressed by your blog.
Hello there, You have done an excellent job. I will certainly digg
it and for my part suggest to my friends.
I am sure they’ll be benefited from this site.