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

一 .背景:

? 可以獲取到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].

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.


首先我們?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)基本滿足需求蛹找。

    ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市哨坪,隨后出現(xiàn)的幾起案子熄赡,更是在濱河造成了極大的恐慌,老刑警劉巖齿税,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件彼硫,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡凌箕,警方通過查閱死者的電腦和手機(jī)拧篮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來牵舱,“玉大人串绩,你說我怎么就攤上這事∥弑冢” “怎么了礁凡?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)慧妄。 經(jīng)常有香客問我顷牌,道長(zhǎng),這世上最難降的妖魔是什么塞淹? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任窟蓝,我火速辦了婚禮,結(jié)果婚禮上饱普,老公的妹妹穿的比我還像新娘运挫。我一直安慰自己状共,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布谁帕。 她就那樣靜靜地躺著峡继,像睡著了一般。 火紅的嫁衣襯著肌膚如雪匈挖。 梳的紋絲不亂的頭發(fā)上鬓椭,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音关划,去河邊找鬼。 笑死翘瓮,一個(gè)胖子當(dāng)著我的面吹牛贮折,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播资盅,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼调榄,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了呵扛?” 一聲冷哼從身側(cè)響起每庆,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎今穿,沒想到半個(gè)月后缤灵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蓝晒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年腮出,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芝薇。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡胚嘲,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出洛二,到底是詐尸還是另有隱情馋劈,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布晾嘶,位于F島的核電站妓雾,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏垒迂。R本人自食惡果不足惜君珠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望娇斑。 院中可真熱鬧策添,春花似錦材部、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至浸颓,卻和暖如春物臂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背产上。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工棵磷, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人晋涣。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓仪媒,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親谢鹊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子算吩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

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