Android 獲取前臺(tái)應(yīng)用

一 .背景:

? 可以獲取到Android設(shè)備當(dāng)前正在顯示的前臺(tái)應(yīng)用(如果可以,精細(xì)到頁面)。

二.風(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無法申請。

  • 另外!因?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)擊,比如頁面抓取,還有就是獲取前臺(tái)應(yīng)用。

? 這里簡單介紹一下如何使用輔助服務(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ī)(小米、魅族、華為等國產(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)上,長按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].

Android shell tricks: ps
  • -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.


首先我們在cmd命令行模式輸入 : adb shell ps 輸出一下信息:

? 然后能很清晰的看見各種包名而且都是系統(tǒng)正在運(yùn)行中的,按照說明如果 **-p **參數(shù)可以列出前臺(tái)進(jìn)程調(diào)度的話,如果我們在切換程序或者對(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)的了,如果沒有就不做任何操作。

測試結(jié)論:

  • 理論上沒什么毛病,但是實(shí)踐中發(fā)現(xiàn)存在不穩(wěn)定的現(xiàn)象,有時(shí)候根本拿不到,有時(shí)候獲取失敗
  • 比如:最常用的微信,打開微信到登錄頁面時(shí)并沒有捕捉到。
  • 因此,該方案存在一些不穩(wěn)定因素

3.5 大招

? 從網(wǎng)絡(luò)上看到一篇老外大神的做法,中文分析博客已經(jīng)丟失,google一下也沒有啥有效因襲。所以只好自己大概猜測和理解了

上代碼:


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)過測試,基本主流應(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)基本滿足需求。

    ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,733評(píng)論 25 709
  • afinalAfinal是一個(gè)android的ioc,orm框架 https://github.com/yangf...
    passiontim閱讀 15,835評(píng)論 2 45
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評(píng)論 19 139
  • 最近好喜歡聽粵語歌啊,誒,不對(duì),不是最近,是這一學(xué)期 最喜歡的張敬軒的, 泳兒聲音好好聽,可是大多是翻唱,可惜 好...
    若看見請回信閱讀 407評(píng)論 0 0
  • 聲明:文中部分內(nèi)容憑個(gè)人記憶,無法確保準(zhǔn)確性(孫大賀) 經(jīng)常聽身邊同學(xué)談到羅永浩和錘子手機(jī),畢竟羅永浩一直是一個(gè)處...
    孫大賀閱讀 265評(píng)論 0 0

友情鏈接更多精彩內(nèi)容