一 .背景:
? 可以獲取到Android設(shè)備當(dāng)前正在顯示的前臺(tái)應(yīng)用(如果可以则北,精細(xì)到頁(yè)面)虐秋。
二.風(fēng)險(xiǎn)點(diǎn)
- 兼容Android 各大版本
- 兼容所有應(yīng)用
三.調(diào)研方案
3.1 Android 5.0之前getRunningTasks
? Android5.0以前,使用ActivityManager的getRunningTasks()方法,可以得到應(yīng)用包名和Activity;
ActivityManager activityManager = (ActivityManager)context.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
ComponentName runningTopActivity = activityManager.getRunningTasks(1).get(0).topActivity;
? 還需要聲明權(quán)限:
<uses-permission android:name="android.permission.GET_TASKS" />
? 這種方法不止能獲取包名,還能獲取Activity名凉蜂。但是在Android 5.0以后,系統(tǒng)就不再對(duì)第三方應(yīng)用提供這種方式來獲取前臺(tái)應(yīng)用了性誉,雖然調(diào)用這個(gè)方法還是能夠返回結(jié)果窿吩,但是結(jié)果只包含你自己的Activity和Launcher了。
具體可見下面的權(quán)限判斷:
private boolean isGetTasksAllowed(String caller, int callingPid, int callingUid) {
boolean allowed = checkPermission(android.Manifest.permission.REAL_GET_TASKS,
callingPid, callingUid) == PackageManager.PERMISSION_GRANTED;
if (!allowed) {
if (checkPermission(android.Manifest.permission.GET_TASKS,
callingPid, callingUid) == PackageManager.PERMISSION_GRANTED) {
// Temporary compatibility: some existing apps on the system image may
// still be requesting the old permission and not switched to the new
// one; if so, we'll still allow them full access. This means we need
// to see if they are holding the old permission and are a system app.
try {
if (AppGlobals.getPackageManager().isUidPrivileged(callingUid)) {
allowed = true;
Slog.w(TAG, caller + ": caller " + callingUid
+ " is using old GET_TASKS but privileged; allowing");
}
} catch (RemoteException e) {
}
}
}
if (!allowed) {
Slog.w(TAG, caller + ": caller " + callingUid
+ " does not hold REAL_GET_TASKS; limiting output");
}
return allowed;
3.2 通過使用量統(tǒng)計(jì)功能獲取前臺(tái)應(yīng)用
? 在StackOverFlow上大多數(shù)的答案都是使用usage statistics API艾栋。
? Android提供了usage statistics API爆存。這個(gè)API本來是系統(tǒng)用來統(tǒng)計(jì)app使用情況的,包含了每個(gè)app最近一次被使用的時(shí)間蝗砾。我們只需要找出距離現(xiàn)在時(shí)間最短的那個(gè)app先较,就是當(dāng)前在前臺(tái)的app。
private String getForegroundApp(Context context) {
UsageStatsManager usageStatsManager =
(UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
long ts = System.currentTimeMillis();
List<UsageStats> queryUsageStats =
usageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_BEST, 0, ts);
UsageEvents usageEvents = usageStatsManager.queryEvents(isInit ? 0 : ts-5000, ts);
if (usageEvents == null) {
return null;
}
UsageEvents.Event event = new UsageEvents.Event();
UsageEvents.Event lastEvent = null;
while (usageEvents.getNextEvent(event)) {
// if from notification bar, class name will be null
if (event.getPackageName() == null || event.getClassName() == null) {
continue;
}
if (lastEvent == null || lastEvent.getTimeStamp() < event.getTimeStamp()) {
lastEvent = event;
}
}
if (lastEvent == null) {
return null;
}
return lastEvent.getPackageName();
}
問題點(diǎn):
- 這種方式只能拿到包名悼粮,無法精確到了Activity了闲勺。
- 使用這種方發(fā)之前,首先要引導(dǎo)用戶開啟使用量功能:
Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
startActivity(intent);
- 還要申明權(quán)限:
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
? 這個(gè)權(quán)限試了下Android Studio 直接提示為系統(tǒng)權(quán)限扣猫,普通App無法申請(qǐng)菜循。
- 另外!因?yàn)樵谝恍┦謾C(jī)上申尤,應(yīng)用發(fā)起通知欄消息的時(shí)候癌幕,或者是下拉通知欄,也會(huì)被記錄到使用量中昧穿,就會(huì)導(dǎo)致按最近時(shí)間排序出現(xiàn)混亂勺远。而且收起通知欄以后,這種混亂并不會(huì)被修正时鸵,而是必須重新開啟一個(gè)應(yīng)用才行胶逢。
到這里基本可以先否定這個(gè)方案了,步驟復(fù)雜饰潜,還需要用戶手動(dòng)開啟權(quán)限初坠,不可能噠!
3.3 通過輔助服務(wù)獲取前臺(tái)應(yīng)用
? Android 輔助服務(wù)(AccessibilityService)有很多神奇的妙用彭雾,比如輔助點(diǎn)擊碟刺,比如頁(yè)面抓取,還有就是獲取前臺(tái)應(yīng)用薯酝。
? 這里簡(jiǎn)單介紹一下如何使用輔助服務(wù)半沽,首先要在AndroidManifest.xml中聲明:
<service
android:name=".service.AccessibilityMonitorService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
>
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility" />
</service>
然后在res/xml/文件夾下新建文件accessibility.xml身诺,內(nèi)容如下:
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeViewClicked|typeViewLongClicked|typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagRetrieveInteractiveWindows"
android:canRetrieveWindowContent="true"
android:canRequestFilterKeyEvents ="true"
android:notificationTimeout="10"
android:packageNames="@null"
android:description="@string/accessibility_des"
android:settingsActivity="com.pl.recent.MainActivity"
/>
關(guān)鍵是typeWindowStateChanged。
新建AccessibilityMonitorService抄囚,主要內(nèi)容如下:
public class AccessibilityMonitorService extends AccessibilityService {
private CharSequence mWindowClassName;
private String mCurrentPackage;
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
int type=event.getEventType();
switch (type){
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
mWindowClassName = event.getClassName();
mCurrentPackage = event.getPackageName()==null?"":event.getPackageName().toString();
break;
case TYPE_VIEW_CLICKED:
case TYPE_VIEW_LONG_CLICKED:
break;
}
}
}
問題點(diǎn)
- 也是需要用戶去在“設(shè)置里”找到輔助服務(wù)并開啟即可。
- 輔助服務(wù)在一些手機(jī)(小米橄务、魅族幔托、華為等國(guó)產(chǎn)手機(jī))上,一旦程序被清理后臺(tái)蜂挪,就會(huì)被關(guān)閉重挑。。棠涮。
so谬哀,這種方案不太穩(wěn)定而且也是需要用戶手動(dòng)去開啟,不可能噠严肪!
3.4 通過設(shè)備輔助應(yīng)用程序獲取前臺(tái)應(yīng)用(比較雞肋)
? 所謂設(shè)備輔助應(yīng)用程序史煎,是在一些接近原生的系統(tǒng)上,長(zhǎng)按Home鍵就會(huì)觸發(fā)的應(yīng)用驳糯,默認(rèn)是會(huì)觸發(fā)Google搜索篇梭。設(shè)備輔助應(yīng)用程序有點(diǎn)像是需要主動(dòng)觸發(fā)的輔助服務(wù),因?yàn)閼?yīng)用中是無法主動(dòng)去觸發(fā)其功能的酝枢,所以說比較雞肋恬偷,
3.5 PS 命令
? 在Android 的ADB命令中我們可以通過PS
命令來獲取到一些應(yīng)用進(jìn)程信息,看下官方解釋:
P show scheduling policy, either bg or fg are common, but also un and er for failures to get policy
大概意思就是說這個(gè)會(huì)列出系統(tǒng)調(diào)度列表帘睦,如果是系統(tǒng)的話袍患,那是不是就說明能夠得到界面的調(diào)度呢?
Android shell tricks: ps
If you ever played around with the adb shell
you may have found that the ps
utility, which lists process lists, is not as verbose as you would expect it to be. And, to make things worse, there’s no inline help or man
entries. Here’s the ps
utility usage line: ps -t -x -P -p -c [pid|name]
.
-
-t
show threads, comes up with threads in the list -
-x
shows time, user time and system time in seconds - -P show scheduling policy, either bg or fg are common, but also un and er for failures to get policy
- -p show priorities, niceness level
-
-c
show CPU (may not be available prior to Android 4.x) involved -
[pid]
filter by PID if numeric, or… -
[name]
…filter by process name
Android’s core toolbox (shell utilities) are more primitive than the ones you may be used to. Notice how each argument needs to be separated and you can’t just -txPc
it all, the command line argument parser is non-complex.
It’s a pity how command line arguments are not shown. If you need something that’s not available by the stock ps
shell utility, try manually combing through the /proc
directory. For the command line one would do cat /proc/<pid>/cmdline
.
首先我們?cè)赾md命令行模式輸入 : adb shell ps 輸出一下信息:
? 然后能很清晰的看見各種包名而且都是系統(tǒng)正在運(yùn)行中的,按照說明如果 **-p **參數(shù)可以列出前臺(tái)進(jìn)程調(diào)度的話,如果我們?cè)谇袚Q程序或者對(duì)出時(shí)包名列表都會(huì)有變化竣付。
以下是輸入 adb shell ps -p 后輸出的信息:
? 仔細(xì)觀察會(huì)發(fā)現(xiàn)u0開頭的都是我們正常程序的包名,而且在程序切換到后臺(tái)以后诡延,這個(gè)列表是有變化的,隨便啟動(dòng)一個(gè)自己安裝的應(yīng)用,列表也剛好出現(xiàn)那個(gè)應(yīng)用。在此大家應(yīng)該就已經(jīng)知道怎么寫了卑笨,這里也提供一下實(shí)現(xiàn)思路:
1孕暇、命令行獲取控制臺(tái)輸出流
2、找出每行輸出的 u0開通的信息獲取包名
3赤兴、用一個(gè)列表存入,與每次獲取的當(dāng)前列表項(xiàng)與上一次列表項(xiàng)對(duì)比妖滔,如果舊的列表不存在此包名,那就證明這個(gè)包就是新啟動(dòng)的了,如果沒有就不做任何操作桶良。
測(cè)試結(jié)論:
- 理論上沒什么毛病座舍,但是實(shí)踐中發(fā)現(xiàn)存在不穩(wěn)定的現(xiàn)象,有時(shí)候根本拿不到陨帆,有時(shí)候獲取失敗
- 比如:最常用的微信曲秉,打開微信到登錄頁(yè)面時(shí)并沒有捕捉到采蚀。
- 因此,該方案存在一些不穩(wěn)定因素
3.5 大招
? 從網(wǎng)絡(luò)上看到一篇老外大神的做法承二,中文分析博客已經(jīng)丟失榆鼠,google一下也沒有啥有效因襲。所以只好自己大概猜測(cè)和理解了
上代碼:
public class SuperRunningPackage {
/** first app user */
public static final int AID_APP = 10000;
/** offset for uid ranges for each user */
public static final int AID_USER = 100000;
public static String getForegroundApp() {
Log.e("PKG","VersionCode:"+Build.VERSION.SDK_INT);
File[] files = new File("/proc").listFiles();
int lowestOomScore = Integer.MAX_VALUE;
String foregroundProcess = null;
for (File file : files) {
if (!file.isDirectory()) {
continue;
}
int pid;
try {
pid = Integer.parseInt(file.getName());
} catch (NumberFormatException e) {
continue;
}
try {
String cgroup = read(String.format("/proc/%d/cgroup", pid));
String[] lines = cgroup.split("\n");
String cpuSubsystem;
String cpuaccctSubsystem;
for (int i = 0; i < lines.length; i++) {
Log.e("PKG",lines[i]);
}
if (lines.length == 2) {//有的手機(jī)里cgroup包含2行或者3行亥鸠,我們?nèi)pu和cpuacct兩行數(shù)據(jù)
cpuSubsystem = lines[0];
cpuaccctSubsystem = lines[1];
}else if(lines.length==3){
cpuSubsystem = lines[0];
cpuaccctSubsystem = lines[2];
}else if(lines.length == 5){//6.0系統(tǒng)
cpuSubsystem = lines[2];
cpuaccctSubsystem = lines[4];
}else {
continue;
}
if (!cpuaccctSubsystem.endsWith(Integer.toString(pid))) {
// not an application process
continue;
}
if (cpuSubsystem.endsWith("bg_non_interactive")) {
// background policy
continue;
}
String cmdline = read(String.format("/proc/%d/cmdline", pid));
if (cmdline.contains("com.android.systemui")) {
continue;
}
int uid = Integer.parseInt(
cpuaccctSubsystem.split(":")[2].split("/")[1].replace("uid_", ""));
if (uid >= 1000 && uid <= 1038) {
// system process
continue;
}
int appId = uid - AID_APP;
int userId = 0;
// loop until we get the correct user id.
// 100000 is the offset for each user.
while (appId > AID_USER) {
appId -= AID_USER;
userId++;
}
if (appId < 0) {
continue;
}
// u{user_id}_a{app_id} is used on API 17+ for multiple user account support.
// String uidName = String.format("u%d_a%d", userId, appId);
File oomScoreAdj = new File(String.format("/proc/%d/oom_score_adj", pid));
if (oomScoreAdj.canRead()) {
int oomAdj = Integer.parseInt(read(oomScoreAdj.getAbsolutePath()));
if (oomAdj != 0) {
continue;
}
}
int oomscore = Integer.parseInt(read(String.format("/proc/%d/oom_score", pid)));
if (oomscore < lowestOomScore) {
lowestOomScore = oomscore;
foregroundProcess = cmdline;
}
} catch (IOException e) {
e.printStackTrace();
}
}
return foregroundProcess;
}
private static String read(String path) throws IOException {
StringBuilder output = new StringBuilder();
BufferedReader reader = new BufferedReader(new FileReader(path));
output.append(reader.readLine());
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
output.append('\n').append(line);
}
reader.close();
return output.toString().trim();
}
}
?
? 不詳細(xì)分析妆够,大概意思就是每次前臺(tái)應(yīng)用變動(dòng)就會(huì)改變一個(gè)配置文件,因此可以通過讀取改配置的方案來獲取前臺(tái)應(yīng)用负蚊。經(jīng)過測(cè)試神妹,基本主流應(yīng)用在前臺(tái)時(shí)都可以捕捉到。
四.大結(jié)論
Android 5.0 以下可以通過
getRunningTasks
獲取到前臺(tái)的包名家妆。Android 5.0-6.0 可以通過讀取系統(tǒng)配置文件來獲取當(dāng)前前臺(tái)應(yīng)用鸵荠。
-
Android 7.0+暫時(shí)不確定穩(wěn)定性,需要后期更多實(shí)踐伤极,我理解7.0 以下已經(jīng)基本滿足需求蛹找。
?