轉(zhuǎn)載注明出處:簡(jiǎn)書-十個(gè)雨點(diǎn)
我目前已知,并且嘗試過(guò)的獲取當(dāng)前前臺(tái)應(yīng)用的方法有如下幾種:
- Android5.0以前,使用ActivityManager的getRunningTasks()方法,可以得到應(yīng)用包名和Activity;
- Android5.0以后,通過(guò)使用量統(tǒng)計(jì)功能來(lái)實(shí)現(xiàn),只能得到應(yīng)用包名;
- 通過(guò)輔助服務(wù)來(lái)實(shí)現(xiàn),可以得到包名和Activity;
- Android5.0以后,可以通過(guò)設(shè)備輔助應(yīng)用程序來(lái)實(shí)現(xiàn),能得到包名和Activity,不過(guò)這種方式必須用戶主動(dòng)觸發(fā)(長(zhǎng)按Home鍵)
一、ActivityManager的getRunningTasks方法
這是大家可能都知道的方法。在Android5.0以前,只要以下代碼就可以獲得前臺(tái)應(yīng)用:
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)用提供這種方式來(lái)獲取前臺(tái)應(yīng)用了,雖然調(diào)用這個(gè)方法還是能夠返回結(jié)果,但是結(jié)果只包含你自己的Activity和Launcher了。
二、通過(guò)使用量統(tǒng)計(jì)功能獲取前臺(tái)應(yīng)用
stackoverflow will find a way,getRunningTasks方法失效以后,基本上搜索到的方案都是通過(guò)使用量統(tǒng)計(jì)功能來(lái)獲取,也就是下面這種方式:
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);
}
}
代碼的功能是通過(guò)UsageStatsManager 來(lái)獲取用戶使用的程序的列表,然后按照最近使用時(shí)間排序,就得到了當(dāng)前的前臺(tái)應(yīng)用,這種方式只能拿到包名,無(wú)法精確了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" />
要注意的是,只是這樣并不夠!因?yàn)樵谝恍┦謾C(jī)上,應(yīng)用發(fā)起通知欄消息的時(shí)候,或者是下拉通知欄,也會(huì)被記錄到使用量中,就會(huì)導(dǎo)致按最近時(shí)間排序出現(xiàn)混亂。而且收起通知欄以后,這種混亂并不會(huì)被修正,而是必須重新開啟一個(gè)應(yīng)用才行。
比如下圖:

圖中應(yīng)用的功能是每隔1秒判斷當(dāng)前的前臺(tái)應(yīng)用,如果是Chrome的話,則把懸浮窗(眼睛圖案)隱藏,否則保持顯示。圖中可見,剛打開Chrome的時(shí)候,懸浮窗隱藏了,但是下拉通知欄,懸浮窗就又出現(xiàn)了。
那怎么辦呢?萬(wàn)能的StackOverflow也不能了,我只好自己研究,通過(guò)仔細(xì)對(duì)比,我發(fā)現(xiàn)UsageStats有一個(gè)hide的字段似乎有些蹊蹺——mLastEvent,如下圖。

我發(fā)現(xiàn)對(duì)于打開的在前臺(tái)的應(yīng)用mLastEvent=1,而對(duì)于通知欄收到消息的應(yīng)用,則mLastEvent!=1,有時(shí)為2,有時(shí)為0。看看UsageStats的源碼,沒發(fā)現(xiàn)有用信息,但是UsageStatsManager還有個(gè)方法queryEvents,返回值是UsageEvents類型,同樣是Event會(huì)不會(huì)有什么相同的地方呢?果然,UsageEvents的內(nèi)部類有一個(gè)Event,它包含兩個(gè)常量定義:
/**
* 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;
此時(shí)我們不妨樂(lè)觀的假設(shè),這兩個(gè)值分別就是UsageStats.mLastEvent中的1和2,從名字上就能看得出含義,正是我們需要的值。
帶著假設(shè)去源碼中尋找答案,會(huì)發(fā)現(xiàn)源碼中充斥著類似下面的代碼:
//下面代碼來(lái)自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就對(duì)應(yīng)著UsageEvents.Event中的常量。那么我們要做的就很簡(jiǎn)單了,只要將queryUsageStats()方法得到的結(jié)果按最后使用時(shí)間降序排列,然后取第一個(gè)mLastEvent ==1的元素即可。代碼和效果圖如下:
//改進(jìn)版本的通過(guò)使用量統(tǒng)計(jì)功能獲取前臺(tái)應(yīng)用
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);
}
}

三、通過(guò)輔助服務(wù)獲取前臺(tái)應(yīng)用
輔助服務(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"
/>
至于其中每一項(xiàng)的內(nèi)容,還是去看API文檔吧,我這里一一解釋的話,文章就太長(zhǎng)了。關(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;
}
}
}
就這么簡(jiǎn)單,就可以獲取當(dāng)前前臺(tái)應(yīng)用的包名和Activity名了。
但是需要注意的是,輔助服務(wù)在一些手機(jī)(小米、魅族、華為等國(guó)產(chǎn)手機(jī))上,一旦程序被清理后臺(tái),就會(huì)被關(guān)閉。。。
想要了解輔助服務(wù)如何監(jiān)控點(diǎn)擊和抓取頁(yè)面的,可以參考Bigbang項(xiàng)目的BigBangMonitorService。
四、通過(guò)設(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)用中是無(wú)法主動(dòng)去觸發(fā)其功能的,所以說(shuō)比較雞肋,鑒于篇幅,這里就不詳細(xì)介紹了。
感興趣的朋友可以看Demo源碼中的簡(jiǎn)單應(yīng)用,也可以看看Bigbang項(xiàng)目的BBVoiceInteractionService、BBVoiceInteractionSession和BBVoiceInteractionSessionService