获取当前顶端包名【兼容所有系统】

获取顶端包名功能在Android 5.0系统前后发生重大变化,经过各种搬运加测试后,得出一些通用解决方案,虽然没有一种简单通用的方法,但是目前已经有可以兼容所有系统的方案。

Android 5.0 之前

直接通过getRunningTasks方法进行获取

用法

需要添加下面权限:

1
<uses-permission android:name="android.permission.GET_TASKS" />
1
2
3
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
ComponentName cn = activityManager.getRunningTasks(1).get(0).topActivity;
String currentPkgName = cn.getPackageName(); // 当前显示在顶端的包名

结论

  1. 通过上面的方法,我们就可以获取到当前显示在顶端的包名,很多锁屏类,或者应用锁的核心就是这块
  2. 可惜的是5.0(API > 20) 之后就不能再用这个方法了,根据下面的官方说明,Android 5.0系统之后 getRunningTasks 方法被弃用了,经过实际测试之后,这个方法仅仅只能返回自身应用信息以及一些公开包名的信息(如:桌面,设置等)

Android 5.0 之后

通过getRunningAppProcesses方法进行获取

原理:通过获取当前在顶端运行的进程,遍历该进程下的所有包名,得出该进程下的包名列表

使用方法

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
/**
* 检查当前在最前端运行的进程包名列表
* @param context
* @return
*/

public final static String[] getForegroundPkgName(Context context) {
try {
ActivityManager activityManager = (ActivityManager) context.getApplicationContext().getSystemService(
Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> processInfos = activityManager.getRunningAppProcesses();
for (ActivityManager.RunningAppProcessInfo processInfo : processInfos) {
if (processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
Log.i("test", "当前顶端进程的信息");
Log.i("test", "进程id: " + processInfo.pid);
Log.i("test", "进程名 : " + processInfo.processName);
Log.i("test", "该进程下可能存在的包名");
for (String pkgName : processInfo.pkgList) {
Log.i("test", " " + pkgName);
}
return processInfo.pkgList;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

结论

  1. 正如原理所描述,它返回的是当前在顶端运行的进程的包名列表,正常情况下,我们开发的时候都是一个进程对应一个包名,但是如果是大公司的话,可能会将旗下所有应用共享同一个进程,于是这里就会返回一个包名数组,所以这个方法只能说满足大部分日常需求
  2. 经过最近的测试,还发现了在一些新的机器上(国产为主,代表:小米 v5.0.2系统)已经不能再使用这种方案了,因为这个方法在这些机器上,只能获取到自己应用的信息,变得和 getRunningTasks 在Android 5.0系统上的表现差不多
  3. 需要继续寻找新方案…

通过Android 5.0提供的新API进行获取

Android 5.0之后提供了一个新的API: UsageStatsManager,通过该API我们可以准确获取到当前顶端的包名信息

使用方法

需要添加下面的权限:

  1. 添加下面权限:
1
2
3
<uses-permission
android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />

  1. 引导用户到设置页面为自身应用开启 有权查看使用情况的应用 的权限:
1
2
3
4
5
6
7
8
9
10
Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
startActivity(intent);

//一些机子上是没法直接到达上面的设置页面的,所以还需要 ``try catch`` 上面的代码,catch到错误之后尝试后备方案——进入到该设置页面的上一级页面

Intent intent = new Intent(Settings.ACTION_SECURITY_SETTINGS);
intent.setComponent(
new ComponentName("com.android.settings", "com.android.settings.Settings$SecuritySettingsActivity"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
  1. 通过下面的代码即可获取顶端包名:
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
61
/**
* 获取当前在顶端运行的应用包名(适用于Andriod 5.0以上的机器)
* <p>
* 通过AndroidL的新API——UsageStatsManager来进行获取,但是需要配置权限和需要用户允许获取
* 所以需要本方法能准确获取,但是体验不太好,建议先用getRunningAppProcesses方法进行获取,不行采用这个方法
* </p>
* <p/>
* 使用本方法之前需要配置下面内容
* <pre>
* 1. 权限配置
* a. 需要在AndroidManifest.xml中配置权限
*
* <uses-permission
* android:name="android.permission.PACKAGE_USAGE_STATS"
* tools:ignore="ProtectedPermissions" />
*
*
* b. 然后还要在AndroidManifest.xml中的manifest标签中配置
* xmlns:tools="http://schemas.android.com/tools"
*
* 2. 需要用户允许这个应用能获取用户数据统计信息的权限
* Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
* startActivity(intent);
* </pre>
*
* @param time_ms 从${time_ms}内找出最近显示在顶端的包名
* @return
*/

public static String getTopRunningPkgNameAboveAndroidL2(Context context, long time_ms) {

// 通过Android 5.0 之后提供的新api来获取最近一段时间内的应用的相关信息
String topPackageName = null;

if (Build.VERSION.SDK_INT >= 21) {

try {

// 根据最近time_ms毫秒内的应用统计信息进行排序获取当前顶端的包名
long time = System.currentTimeMillis();
UsageStatsManager usage = (UsageStatsManager) context.getSystemService("usagestats");
List<UsageStats> usageStatsList = usage.queryUsageStats(UsageStatsManager.INTERVAL_BEST, time - time_ms, time);
if (usageStatsList != null && usageStatsList.size() > 0) {
SortedMap<Long, UsageStats> runningTask = new TreeMap<Long, UsageStats>();
for (UsageStats usageStats : usageStatsList) {
runningTask.put(usageStats.getLastTimeUsed(), usageStats);
}
if (runningTask.isEmpty()) {
return null;
}
topPackageName = runningTask.get(runningTask.lastKey()).getPackageName();
Log.i("test", "##当前顶端应用包名:" + topPackageName);
}
}

} catch (Throwable e) {
e.printStackTrace();
}
}

return topPackageName;
}

如果希望能检测用户是否已经允许本应用有权查看组件使用情况,可以通过下面代码进行检查:

1
2
3
4
5
6
7
8
if (Build.VERSION.SDK_INT >= 19) {
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;
}

结论

  1. 使用这个方法之前需要用户允许应用有权查看组件使用情况,然后才有数据返回
  2. 新的API能完美进行,就是需要添加新的权限和做一下用户引导
  3. 如果获取的时间设置得太短,并且这个时间内又没有发生改变的话,那么是获取不到最新的顶端的包名情况的
  4. 最重要的是,最近又发现在一些机器上(国产为主,魅族领头,LG G3在列),是没有允许应用有权查看使用组件的情况的设置页面的,于是,我们大喊一声,妈蛋~
  5. 需要继续寻找新方案…

总结(截止至2015-09-22)

目前还不能全面兼容Android 5.0系统,还需要继续寻找新的方案

后续尝试

尝试dunpsys命令

如果是在adb shell中使用 dumpsys activity activities 的话,倒是可以获取到顶端的包名,但是代码中的话,除非你拥有和系统应用的签名,否则,是不能使用 android.permission.DUMP 权限的,也就不能使用 dumpsys 命令了

continue…

尝试通过/proc目录进行判断

/proc 目录是存放当前系统运行中的一些信息,包括各个进程,cpu,内存,启动时长等,具体目录介绍可以参照这里,偶然间发现了stackoverflow 上的这个神贴,就是用这个方法的,无须任何权限,但是作者也说到缺乏测试,而我这边自己测试之后发现不是很理想,很多机子都不行

ps:

  1. 正常的 cgroup 文件(Nexus4)上结果如下:

  2. 附上几张在天朝不同机子上的 cgroup 文件解读图:

  • 魅族mx4 Flyme OS 4.2.8.2C Android 4.4.2
  • 魅族mx4 pro Flyme OS 4.5.5A Android 5.0.1
  • 魅族m2 note Flyme OS 4.5.3A Android 5.1
  • 小米2s MIUI 5.7.16 Android 5.0.2

continue…

通过accessibility-service服务进行

方法参考这里,经过测试,能够不用在配置新权限的前提下完美运行,但是也有一些问题

  1. Each user must enable the service via Android’s accessibility settings in order for the AccessibilityEvents to be received.
    The service generally runs until the user explicitly disables it. You can make the service stop itself if needed, but I don’t know of any way to programmatically restart it correctly if you’ve stopped it.
  2. When a user tries to enables the AccessibilityService, they can’t press the OK button if an app has placed an overlay on the screen. Some apps that do this are Velis Auto Brightness and Lux. This can be confusing because the user might not know why they can’t press the button or how to work around it.
  3. The AccessibilityService won’t know the current activity until the first change of activity.

continue…

结语

总结下来,Android 5.0以上的机子目前有4种方法可以尝试,每种方法都有一些优点和缺陷,在找到一个完美的方法之前,我们可以依次采用下面的方法来尽量覆盖多一点机型

  1. getRunningAppProcess
  2. 通过观察proc
  3. UsageStatsManager
  4. Accessibility-service

后记&&最终解决方案

在我准备放弃的时候,我又重新看了一下 通过观察proc 获取当前运行中的进程这个方法,发现其核心代码在于通过判断oom-score来识别当前运行的顶端应用的,这个是什么原理呢?查阅了一下资料,这个涉及到 Linux OOM Killer机制:

Linux 内核存在一个OOM killer(Out-Of-Memory killer),该机制会监控那些占用内存过大,尤其是瞬间很快消耗大量内存的进程,为了防止内存耗尽而内核会把该进程杀掉,而每个进程都有一个oom_score属性,oomkiller会杀掉oom_score较大的进程

那么究竟杀掉那个进程呢,其实是受下面3个文件的影响:

  • /proc/$PID/oom_adj:用于调整oom_score的分数,范围在[-17,15],当为-17时,oom_score将变为0,标识禁止杀死该进程
  • /proc/$PID/oom_score:标识最终的分数,分数越大,进程越有可能被杀
  • /proc/$PID/oom_score_adj:从Linux 2.6.36开始都安装了/proc/$PID/oom_score_adj,此后替换为/proc/$PID/oom_adj,即使当前是对/proc/$PID/oom_adj进行的设置,在内核内部进行变换后的值也是针对/proc/$PID/oom_score_adj设置的

3个文件的具体manpage可以参考这里,下面是我截取的一些关键内容

/proc/[pid]/oom_adj (since Linux 2.6.11)

This file can be used to adjust the score used to select which process should be killed in an out-of-memory (OOM) situation. The kernel uses this value for a bit-shift operation of the process’s oom_score value: valid values are in the range -16 to +15, plus the special value -17, which disables OOM-killing altogether for this process. A positive score increases the likelihood of this process being killed by the OOM-killer; a negative score decreases the likelihood.

The default value for this file is 0; a new process inherits its parent’s oom_adj setting. A process must be privileged (CAP_SYS_RESOURCE) to update this file. Since Linux 2.6.36, use of this file is deprecated in favor of /proc/[pid]/oom_score_adj.

/proc/[pid]/oom_score (since Linux 2.6.11)

This file displays the current score that the kernel gives to this process for the purpose of selecting a process for the OOM-killer. A higher score means that the process is more likely to be selected by the OOM-killer. The basis for this score is the amount of memory used by the process, with increases (+) or decreases (-) for factors including:

  • whether the process creates a lot of children using fork(2) (+);
  • whether the process has been running a long time, or has used a lot of CPU time (-);
  • whether the process has a low nice value (i.e., > 0) (+);
  • whether the process is privileged (-); and
  • whether the process is making direct hardware access (-).

The oom_score also reflects the adjustment specified by the oom_score_adj or oom_adj setting for the process.

/proc/[pid]/oom_score_adj (since Linux 2.6.36)
This file can be used to adjust the badness heuristic used to select which process gets killed in out-of-memory conditions. The badness heuristic assigns a value to each candidate task ranging from 0 (never kill) to 1000 (always kill) to determine which process is targeted. The units are roughly a proportion along that range of allowed memory the process may allocate from, based on an estimation of its current memory and swap use. For example, if a task is using all allowed memory, its badness score will be 1000. If it is using half of its allowed memory, its score will be 500.

There is an additional factor included in the badness score: root processes are given 3% extra memory over other tasks.

The amount of “allowed” memory depends on the context in which the OOM-killer was called. If it is due to the memory assigned to the allocating task’s cpuset being exhausted, the allowed memory represents the set of mems assigned to that cpuset (see cpuset(7)). If it is due to a mempolicy’s node(s) being exhausted, the allowed memory represents the set of mempolicy nodes. If it is due to a memory limit (or swap limit) being reached, the allowed memory is that configured limit. Finally, if it is due to the entire system being out of memory, the allowed memory represents all allocatable resources.

The value of oom_score_adj is added to the badness score before it is used to determine which task to kill. Acceptable values range from -1000 (OOM_SCORE_ADJ_MIN) to +1000 (OOM_SCORE_ADJ_MAX). This allows user space to control the preference for OOM-killing, ranging from always preferring a certain task or completely disabling it from OOM killing. The lowest possible value, -1000, is equivalent to disabling OOM- killing entirely for that task, since it will always report a badness score of 0.

Consequently, it is very simple for user space to define the amount of memory to consider for each task. Setting a oom_score_adj value of +500, for example, is roughly equivalent to allowing the remainder of tasks sharing the same system, cpuset, mempolicy, or memory controller resources to use at least 50% more memory. A value of -500, on the other hand, would be roughly equivalent to discounting 50% of the task’s allowed memory from being considered as scoring against the task.

For backward compatibility with previous kernels, /proc/[pid]/oom_adj can still be used to tune the badness score. Its value is scaled linearly with oom_score_adj.

Writing to /proc/[pid]/oom_score_adj or /proc/[pid]/oom_adj will change the other with its scaled value.

通过上面的介绍,估计大家都有些想法了,恩,这是一条可行的路:

  1. 遍历/proc目录下的子目录,排除系统应用uid
  2. 在根据一些其他规则排除一些不符合的应用
  3. 排序剩下的应用,得出oom_score分数最少的pid
  4. 根据pid找对应的包名

原理大概如此,基本代码可以参考这里,但是照抄那份代码是不太能在天朝厂商机子上跑通的,需要改进。Anyway,最后我改进代码之后,在我这边手上几台机子测试都没有什么问题,目前也在大规模测试中。

ps. 因为一些原因,最终代码还不太好在现在发放出来

参考资料