4种获取前台应用的方法(肯定有你不知道的)

转载注明出处:简书-十个雨点

我目前已知,并且尝试过的获取当前前台应用的方法有如下几种:

  1. Android5.0以前,使用ActivityManager的getRunningTasks()方法,可以得到应用包名和Activity;
  2. Android5.0以后,通过使用量统计功能来实现,只能得到应用包名;
  3. 通过辅助服务来实现,可以得到包名和Activity;
  4. Android5.0以后,可以通过设备辅助应用程序来实现,能得到包名和Activity,不过这种方式必须用户主动触发(长按Home键)

一、ActivityManager的getRunningTasks方法

这是大家可能都知道的方法。在Android5.0以前,只要以下代码就可以获得前台应用:

ActivityManager activityManager = (ActivityManager)context.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
ComponentName runningTopActivity = activityManager.getRunningTasks(1).get(0).topActivity;

还需要声明权限:

<uses-permission android:name="android.permission.GET_TASKS" />

这种方法不止能获取包名,还能获取Activity名。但是在Android 5.0以后,系统就不再对第三方应用提供这种方式来获取前台应用了,虽然调用这个方法还是能够返回结果,但是结果只包含你自己的Activity和Launcher了。

二、通过使用量统计功能获取前台应用

stackoverflow will find a way,getRunningTasks方法失效以后,基本上搜索到的方案都是通过使用量统计功能来获取,也就是下面这种方式:

UsageStatsManager mUsageStatsManager = (UsageStatsManager)context.getApplicationContext().getSystemService(Context.USAGE_STATS_SERVICE);
long time = System.currentTimeMillis();
List<UsageStats> stats ;
if (isFirst){
    stats = mUsageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - TWENTYSECOND, time);
}else {
    stats = mUsageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - THIRTYSECOND, time);
}
// Sort the stats by the last time used
if(stats != null) {
    TreeMap<Long,UsageStats> mySortedMap = new TreeMap<Long,UsageStats>();
    start=System.currentTimeMillis();
    for (UsageStats usageStats : stats) {
        mySortedMap.put(usageStats.getLastTimeUsed(),usageStats);
    }
    LogUtil.e(TAG,"isFirst="+isFirst+",mySortedMap cost:"+ (System.currentTimeMillis()-start));
    if(mySortedMap != null && !mySortedMap.isEmpty()) {                    
        
        topPackageName =  mySortedMap.get(mySortedMap.lastKey()).getPackageName();      
        
        runningTopActivity=new ComponentName(topPackageName,"");
        if (LogUtil.isDebug())LogUtil.d(TAG,topPackageName);
    }
}

代码的功能是通过UsageStatsManager 来获取用户使用的程序的列表,然后按照最近使用时间排序,就得到了当前的前台应用,这种方式只能拿到包名,无法精确了Activity了。
使用这种方发之前,首先要引导用户开启使用量功能:

Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
startActivity(intent);

还要申明权限:

<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />

要注意的是,只是这样并不够!因为在一些手机上,应用发起通知栏消息的时候,或者是下拉通知栏,也会被记录到使用量中,就会导致按最近时间排序出现混乱。而且收起通知栏以后,这种混乱并不会被修正,而是必须重新开启一个应用才行。
比如下图:

打开通知栏导致前台应用判断错误.gif

图中应用的功能是每隔1秒判断当前的前台应用,如果是Chrome的话,则把悬浮窗(眼睛图案)隐藏,否则保持显示。图中可见,刚打开Chrome的时候,悬浮窗隐藏了,但是下拉通知栏,悬浮窗就又出现了。

那怎么办呢?万能的StackOverflow也不能了,我只好自己研究,通过仔细对比,我发现UsageStats有一个hide的字段似乎有些蹊跷——mLastEvent,如下图。

新建位图图像_看图王.jpg

我发现对于打开的在前台的应用mLastEvent=1,而对于通知栏收到消息的应用,则mLastEvent!=1,有时为2,有时为0。看看UsageStats的源码,没发现有用信息,但是UsageStatsManager还有个方法queryEvents,返回值是UsageEvents类型,同样是Event会不会有什么相同的地方呢?果然,UsageEvents的内部类有一个Event,它包含两个常量定义:

/**
 * An event type denoting that a component moved to the foreground.
 */
public static final int MOVE_TO_FOREGROUND = 1;

/**
 * An event type denoting that a component moved to the background.
 */
public static final int MOVE_TO_BACKGROUND = 2;

此时我们不妨乐观的假设,这两个值分别就是UsageStats.mLastEvent中的1和2,从名字上就能看得出含义,正是我们需要的值。
带着假设去源码中寻找答案,会发现源码中充斥着类似下面的代码:

//下面代码来自com.android.server.usage.IntervalStats
private boolean isStatefulEvent(int eventType) {
   switch (eventType) {
       case UsageEvents.Event.MOVE_TO_FOREGROUND:
       case UsageEvents.Event.MOVE_TO_BACKGROUND:
       case UsageEvents.Event.END_OF_DAY:
       case UsageEvents.Event.CONTINUE_PREVIOUS_DAY:
          return true;
   }
   return false;
}

void update(String packageName, long timeStamp, int eventType) {
  UsageStats usageStats = getOrCreateUsageStats(packageName);

  // TODO(adamlesinski): Ensure that we recover from incorrect event sequences
   // like double MOVE_TO_BACKGROUND, etc.
   if (eventType == UsageEvents.Event.MOVE_TO_BACKGROUND ||
           eventType == UsageEvents.Event.END_OF_DAY) {
       if (usageStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
               usageStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
           usageStats.mTotalTimeInForeground += timeStamp - usageStats.mLastTimeUsed;
       }
   }

   if (isStatefulEvent(eventType)) {
       usageStats.mLastEvent = eventType;
   }

   if (eventType != UsageEvents.Event.SYSTEM_INTERACTION) {
       usageStats.mLastTimeUsed = timeStamp;
   }
   usageStats.mLastTimeSystemUsed = timeStamp;
   usageStats.mEndTimeStamp = timeStamp;

   if (eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
       usageStats.mLaunchCount += 1;
   }

   endTime = timeStamp;
}

可见 usageStats.mLastEvent就对应着UsageEvents.Event中的常量。那么我们要做的就很简单了,只要将queryUsageStats()方法得到的结果按最后使用时间降序排列,然后取第一个mLastEvent ==1的元素即可。代码和效果图如下:

//改进版本的通过使用量统计功能获取前台应用
UsageStatsManager mUsageStatsManager = (UsageStatsManager)context.getApplicationContext().getSystemService(Context.USAGE_STATS_SERVICE);
long time = System.currentTimeMillis();
List<UsageStats> stats ;
if (isFirst){
    stats = mUsageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - TWENTYSECOND, time);
}else {
    stats = mUsageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - THIRTYSECOND, time);
}
// Sort the stats by the last time used
if(stats != null) {
    TreeMap<Long,UsageStats> mySortedMap = new TreeMap<Long,UsageStats>();
    start=System.currentTimeMillis();
    for (UsageStats usageStats : stats) {
        mySortedMap.put(usageStats.getLastTimeUsed(),usageStats);
    }
    LogUtil.e(TAG,"isFirst="+isFirst+",mySortedMap cost:"+ (System.currentTimeMillis()-start));
    if(mySortedMap != null && !mySortedMap.isEmpty()) {                   

        NavigableSet<Long> keySet=mySortedMap.navigableKeySet();
        Iterator iterator=keySet.descendingIterator();
        while(iterator.hasNext()){
            UsageStats usageStats = mySortedMap.get(iterator.next());
            if (mLastEventField==null) {
                try {
                    mLastEventField = UsageStats.class.getField("mLastEvent");
                } catch (NoSuchFieldException e) {
                    break;
                }
            }
            if (mLastEventField!=null) {
                int lastEvent = 0;
                try {
                    lastEvent = mLastEventField.getInt(usageStats);
                } catch (IllegalAccessException e) {
                    break;
                }
                if (lastEvent==1){
                    topPackageName=usageStats.getPackageName();
                    break;
                }
            }else {
                break;
            }
        }    
        if (topPackageName==null){
            topPackageName =  mySortedMap.get(mySortedMap.lastKey()).getPackageName();
        }
        runningTopActivity=new ComponentName(topPackageName,"");
        if (LogUtil.isDebug())LogUtil.d(TAG,topPackageName);
    }
}
改进后不会受通知栏影响了

三、通过辅助服务获取前台应用

辅助服务(AccessibilityService)有很多神奇的妙用,比如辅助点击,比如页面抓取,还有就是获取前台应用。
这里简单介绍一下如何使用辅助服务,首先要在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,内容如下:

<?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"
/>

至于其中每一项的内容,还是去看API文档吧,我这里一一解释的话,文章就太长了。关键是typeWindowStateChanged。
新建AccessibilityMonitorService,主要内容如下:

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;
        }
    }
}

就这么简单,就可以获取当前前台应用的包名和Activity名了。
但是需要注意的是,辅助服务在一些手机(小米、魅族、华为等国产手机)上,一旦程序被清理后台,就会被关闭。。。

想要了解辅助服务如何监控点击和抓取页面的,可以参考Bigbang项目的BigBangMonitorService。

四、通过设备辅助应用程序获取前台应用(比较鸡肋)

所谓设备辅助应用程序,是在一些接近原生的系统上,长按Home键就会触发的应用,默认是会触发Google搜索。设备辅助应用程序有点像是需要主动触发的辅助服务,因为应用中是无法主动去触发其功能的,所以说比较鸡肋,鉴于篇幅,这里就不详细介绍了。
感兴趣的朋友可以看Demo源码中的简单应用,也可以看看Bigbang项目的BBVoiceInteractionService、BBVoiceInteractionSession和BBVoiceInteractionSessionService

Demo源码

Github

免费授权转载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容