1. 程式人生 > >Android--判斷App處於前臺還是後臺的方案

Android--判斷App處於前臺還是後臺的方案

很多場景下,都需要判斷某個App處於前臺還是後臺。本文集網上編寫的前臺判斷方案於一體。

目前,有6種方案:

 法

判斷原理 需要許可權 可以判斷其他應用位於前臺 特點
RunningTask Andorid4.0系列可以,5.0以上機器不行 Android5.0此方法被廢棄
RunningProcess 當App存在後臺常駐的Service時失效
ActivityLifecycleCallbacks 簡單有效,程式碼最少
UsageStatsManager 需要使用者手動授權
AccessibilityService 需要使用者手動授權
自解析/process 當/proc目錄下的檔案過多時,過多的IO操作會引起耗時

接下來,就對以上6種方法展開詳細說明:

目錄

1.1 原理

2.1原理

3.1 原理

4.1 原理

5.1 原理

6.1 原理

6.2 優點

6.3 用法

1. RunningTask

1.1 原理

當一個App處於前臺時,會處於RunningTask這個棧的棧頂,所以可以取出RunningTask棧頂的任務程序,與需要判斷的App的包名進行比較,來達到目的。

1.2 程式碼實現

這種方法不僅能獲取到前臺程序的包名還能獲取到activity名稱。

public String getForegroundActivity() {  
    ActivityManager mActivityManager =  
        (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);  
        if (mActivityManager.getRunningTasks(1) == null) {  
            Log.e(TAG, "running task is null, ams is abnormal!!!");  
            return null;  
        }  
        ActivityManager.RunningTaskInfo mRunningTask =  
                    mActivityManager.getRunningTasks(1).get(0);  
        if (mRunningTask == null) {  
            Log.e(TAG, "failed to get RunningTaskInfo");  
            return null;  
        }  
   
        String pkgName = mRunningTask.topActivity.getPackageName();  
        //String activityName =  mRunningTask.topActivity.getClassName();  
        return pkgName;  
}  

1.3 方案缺點

getRunningTask方法在5.0以上已經被廢棄,只會返回自己和系統的一些不敏感的task,不再返回其他應用的task,用CI方法來判斷自身App是否處於後臺仍然有效,但是無法判斷其他應用是否位於前臺,因為不能再獲取資訊。

2. RunningProcess

2.1原理

通過runningProcess獲取到一個當前正在執行的程序的List,我們遍歷這個List中的每一個程序,判斷這個程序的一個importance 屬性是否是前臺程序,並且包名是否與我們判斷的APP的包名一樣,如果這兩個條件都符合,那麼這個App就處於前臺。

2.2  程式碼實現

以下code是判斷當前應用是否在前臺:

private static boolean isAppForeground(Context context) {
    ActivityManager activityManager =     
                       (ActivityManager)context.getSystemService(Service.ACTIVITY_SERVICE);
    List<ActivityManager.RunningAppProcessInfo> runningAppProcessInfoList =           		    	     
                                            activityManager.getRunningAppProcesses();
    if (runningAppProcessInfoList == null) {
        Log.d(TAG,"runningAppProcessInfoList is null!");
        return false;
    }

   for(ActivityManager.RunningAppProcessInfo processInfo : runningAppProcessInfoList) {
        if (processInfo.processName.equals(context.getPackageName())
                &&(processInfo.importance == 
                         ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND)) { 
                return true;
        }
   } 
   return false;
}

以下code是判斷在前臺的是哪個應用:

public String getForegroundApp(Context context) {  
    ActivityManager am =  
        (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);  
    List<RunningAppProcesInfo> lr = am.getRunningAppProcesses();  
    if (lr == null) {  
        return null;  
    }  
  
    for (RunningAppProcessInfo ra : lr) {  
        if (ra.importance == RunningAppProcessInfo.IMPORTANCE_VISIBLE  
            || ra.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {  
            return ra.processName;  
        }  
    }  
    return null;  
}  

getRunningAppProcess方法只能獲取前臺包名。

2.3 方案缺點

Android5.0之後已經被廢棄。

例如,在聊天型別的App中,常常需要常駐後臺來不間斷地獲取伺服器的訊息,就需要把Service設定成START_STICKY,kill後會被重啟(等待5s左右)來保證Service常駐後臺。如果Service設定了這個屬性,這個App的程序就會被判斷為前臺。程式碼表現為

appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND

  上述code永遠成立,這樣就永遠無法判斷到底那個是前臺了。

3.ActivityLifecycleCallbacks

3.1 原理

AndroidSDK14Application類裡增加了ActivityLifecycleCallbacks,我們可以通過這個Callback拿到App所有Activity的生命週期回撥。

 public interface ActivityLifecycleCallbacks {
        void onActivityCreated(Activity activity, Bundle savedInstanceState);
        void onActivityStarted(Activity activity);
        void onActivityResumed(Activity activity);
        void onActivityPaused(Activity activity);
        void onActivityStopped(Activity activity);
        void onActivitySaveInstanceState(Activity activity, Bundle outState);
        void onActivityDestroyed(Activity activity);
  }

知道這些資訊,我們就可以用更官方的辦法來解決問題,只需要在ApplicationonCreate()裡去註冊上述介面,然後由Activity回調回來執行狀態即可。

       Android應用開發中一般認為back鍵是可以捕獲的,而Home鍵是不能捕獲的(除非修改framework,但是上述方法從Activity生命週期著手解決問題,雖然這兩種方式的Activity生命週期並不相同,但是二者都會執行onStop();所以並不關心到底是觸發了哪個鍵切入後臺的。另外,Application是否被銷燬,都不會影響判斷的正確性

3.2 程式碼實現

    (1)AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="mytest.example.com.broadcaststudy">

    <application
        android:name=".TestActivityLifecycleApplcation"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".SendBroadcastActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity android:name=".SecondActivity">
        </activity>
    </application>

</manifest>

(2) TestActivityLifecycleApplication.java

package mytest.example.com.broadcaststudy;

import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import android.util.Log;

/**
 * Created by Maureen on 2018/1/18.
 */
public class TestActivityLifecycleApplcation extends Application {
    private final String TAG = "TestActivityLifecycleApplcation";
    private static TestActivityLifecycleApplcation mTestActivityLifecycleApplcation;
    private int mActivityCount = 0;
    @Override
    public void onCreate() {
        super.onCreate();
        mTestActivityLifecycleApplcation = new TestActivityLifecycleApplcation();

        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                Log.d(TAG,"onActivityCreated");
            }

            @Override
            public void onActivityStarted(Activity activity) {
                Log.d(TAG,"onActivityStarted");
                mActivityCount++;
            }

            @Override
            public void onActivityResumed(Activity activity) {
                Log.d(TAG,"onActivityResumed");
            }

            @Override
            public void onActivityPaused(Activity activity) {
                Log.d(TAG,"onActivityPaused");
            }

            @Override
            public void onActivityStopped(Activity activity) {
                Log.d(TAG,"onActivityStopped");
                mActivityCount--;
            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
                Log.d(TAG,"onActivitySaveInstanceState");
            }

            @Override
            public void onActivityDestroyed(Activity activity) {
                Log.d(TAG,"onActivityDestroyed");
            }
        });
    }

    public static TestActivityLifecycleApplcation getInstance( ) {
        if (null == mTestActivityLifecycleApplcation)
            mTestActivityLifecycleApplcation = new TestActivityLifecycleApplcation();
        return mTestActivityLifecycleApplcation;
    }

    public int getActivityCount( ) {
        return mActivityCount;
    }
}

(3) SendActivity.java

package mytest.example.com.broadcaststudy;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

/**
 * Created by ATC6111 on 2018/1/18.
 */
public class SecondActivity extends Activity {
    private final String TAG = "SecondActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d(TAG, "onStart");
    }

    @Override
    protected void onRestart() {
        super.onRestart();
        Log.d(TAG, "onRestart");
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG, "onResume");

    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG, "onPause");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
    }
}

(4) SendBroadcastActivity.java

package mytest.example.com.broadcaststudy;

import android.app.Activity;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import java.util.List;

public class SendBroadcastActivity extends Activity {
    private static final String TAG = "SendBroadcastActivity";
    private static final String ACTION_MAUREEN_TEST = "com.maureen.test";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG,"onCreate");
        setContentView(R.layout.activity_main);

        Intent intent = new Intent();
        intent.setAction(ACTION_MAUREEN_TEST);
        //intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
        Log.d(TAG,"Begin to sendBroadcast:");
        sendBroadcast(intent);
        Log.d(TAG,"Send broadcast end!");
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d(TAG,"onStart");
    }

    @Override
    protected void onRestart() {
        super.onRestart();
        Log.d(TAG,"onRestart");
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG,"onResume");
        startActivity(new Intent(this, SecondActivity.class));
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG,"onPause");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d(TAG,"onStop");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d(TAG,"onDestroy");
    }
}

在講解為什麼不在onActivityResumed( )與onActivityPaused( )中對activity進行計數,而是在onActivityStarted()和onActivityStopped( )中對activity進行計數之前。

先看以下幾種情況的activity生命週期:

A、啟動App,進入SendBroadcastActivity:

01-01 05:33:56.401 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityCreated
01-01 05:33:56.401 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onCreate
01-01 05:33:56.427 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: Begin to sendBroadcast:
01-01 05:33:56.430 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: Send broadcast end!
01-01 05:33:56.431 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityStarted
01-01 05:33:56.431 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onStart
01-01 05:33:56.432 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityResumed
01-01 05:33:56.432 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onResume

B、點選back鍵退出SendBroadcastActivity:

點選back鍵退出App:
01-01 05:35:37.983 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityPaused
01-01 05:35:37.983 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onPause
01-01 05:35:38.035 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityStopped
01-01 05:35:38.036 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onStop
01-01 05:35:38.036 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityDestroyed
01-01 05:35:38.036 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onDestroy

C、在A情況下點選home鍵:

01-01 05:37:34.690 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityPaused
01-01 05:37:34.690 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onPause
01-01 05:37:34.708 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivitySaveInstanceState
01-01 05:37:34.708 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityStopped
01-01 05:37:34.708 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onStop

D、在A情況下點選recent鍵kill程序:

從recent中kill程序:
01-01 05:38:17.867 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityPaused
01-01 05:38:17.867 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onPause
01-01 05:38:17.914 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivitySaveInstanceState
01-01 05:38:17.914 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityStopped
01-01 05:38:17.914 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onStop

E、新增code在SendBroadcastActivity的onResume( )中啟動SecondActivity:

在SendBroadcastActivity的onResume函式中啟動SecondActivity:
01-01 05:57:05.262 16836-16836/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onCreate
01-01 05:57:05.286 16836-16836/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onStart
01-01 05:57:05.287 16836-16836/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onResume
01-01 05:57:05.324 16836-16836/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onPause
01-01 05:57:05.367 16836-16836/mytest.example.com.broadcaststudy D/SecondActivity: onCreate
01-01 05:57:05.370 16836-16836/mytest.example.com.broadcaststudy D/SecondActivity: onStart
01-01 05:57:05.370 16836-16836/mytest.example.com.broadcaststudy D/SecondActivity: onResume
01-01 05:57:05.605 16836-16836/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onStop

其中尤其是E,可以看到activity切換時的生命週期。這裡暫且稱SendBroadcastActivity為A,SecondActivity為B。

如果在A.onResume( )時mActivityCount = 1; A.onPause()時mActivityCount--,

在B.onResume( )時mActivityCount++,此處就會有個短暫的延時,在跳轉過程中就會出現mActivityCount = 0,

即判斷App在後臺,就不正確了。

所以要在onActivityStart( )和onActivityStopped( )中 對activity進行計數。

即在A.onStart()時mActivity = 1; B.onStart( )時mActivity ++; A.onStop()時,mActivity--。

所以,當activity的計數為0時表示應用在後臺,否則就在前臺。

3.3 方案特點

(1)Android應用開發中,一般認為back鍵是可以捕獲的,而Home鍵不能捕獲(除非修改Framework),雖然這兩種方式的Activity生命週期並不相同,但是二者都會執行onStop( );所以並不關心到底是哪個鍵切入後臺的。另外,Application是否銷燬,都不會影響判斷的正確性;

(2)該方案除了用於判斷當前應用內的哪個activity位於前臺外,還可用於作為實現“程序完全退出”的一種很好的計數方案;

(3)該方案需要在Application中進行註冊相關Activity生命週期的回撥,上述code所示。只需要對mActivityCount計數進行判斷即可知道是否在前臺。

4. UsageStatsManager

4.1 原理

通過使用UsageStatsManager獲取,此方法是Android5.0之後提供的新API,可以獲取一個時間段內的應用統計資訊,但是

必須滿足以下要求。

使用前提

  1. 此方法只在android5.0以上有效
  2. AndroidManifest中加入此許可權
    <uses-permission  android:name="android.permission.PACKAGE_USAGE_STATS" />
  1. 開啟手機設定,點選安全-高階,在有權檢視使用情況的應用中,為這個App打上勾

4.2 程式碼實現

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

跳轉到“檢視應用使用許可權”介面的跳轉程式碼如下:

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

還要宣告許可權:

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

4.3 方案特點

1、該方案最大的缺點是需要使用者手動授權,因此在使用時要結合場景做適當引導;

2、該方案為Android5.0以後Google官方比較推薦的獲取程序資訊的方式,是最符合Google意圖的方式,不過在使用時會有一些延時需要小心處理。

5.  AccessibilityService

5.1 原理

Android 輔助功能(AccessibilityService) 為我們提供了一系列的事件回撥,幫助我們指示一些使用者介面的狀態變化。

我們可以派生輔助功能類,進而對不同的 AccessibilityEvent 進行處理。同樣的,這個服務就可以用來判斷當前的前臺應用

優勢

  • AccessibilityService 有非常廣泛的 ROM 覆蓋,特別是非國產手機,從 Android API Level 18(Android 2.2) 到 Android Api Level 23(Android 6.0)
  • AccessibilityService 不再需要輪詢的判斷當前的應用是不是在前臺,系統會在視窗狀態發生變化的時候主動回撥,耗時和資源消耗都極小
  • 不需要許可權請求
  • 它是一個穩定的方法,與 “方法5”讀取 /proc 目錄不同,它並非利用 Android 一些設計上的漏洞,可以長期使用的可能很大
  • 可以用來判斷任意應用甚至 Activity, PopupWindow, Dialog 物件是否處於前臺

劣勢

  • 需要要使用者開啟輔助功能
  • 輔助功能會伴隨應用被“強行停止”而剝奪

5.2 程式碼實現

步驟:

(1)派生AccessibilityService,建立視窗狀態探測服務

建立DetectionService.java

public class DetectionService extends AccessibilityService {

    final static String TAG = "DetectionService";

    static String foregroundPackageName;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return 0; // 根據需要返回不同的語義值
    }

    /**
     * 過載輔助功能事件回撥函式,對視窗狀態變化事件進行處理
     * @param event
     */
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
            /*
             * 如果 與 DetectionService 相同程序,直接比較 foregroundPackageName 的值即可
             * 如果在不同程序,可以利用 Intent 或 bind service 進行通訊
             */
            foregroundPackageName = event.getPackageName().toString();

            /*
             * 基於以下還可以做很多事情,比如判斷當前介面是否是 Activity,是否系統應用等,
             * 與主題無關就不再展開。
             */
            ComponentName cName = new ComponentName(event.getPackageName().toString(),
                    event.getClassName().toString());
        }
    }

    @Override
    public void onInterrupt() {
    }

    @Override
    protected  void onServiceConnected() {
        super.onServiceConnected();
    }
}

(2)建立Accessibility Service Info屬性檔案

建立res/xml/detection_service_config.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- 根據的 Service 的不同功能需要,你可能需要不同的配置 -->
<accessibility-service
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeWindowStateChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagIncludeNotImportantViews" />

(3)註冊Detection service到AndroidManifest.xml

在AndroidManifest.xml中新增

<service
    android:name="your_package.DetectionService"
    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/detection_service_config"/>

</service>

(4)使用detection service判斷應用是否在前臺

建立isForegroundPkgViaDetectionService( )函式

  /**
     * 方法6:使用 Android AccessibilityService 探測視窗變化,跟據系統回傳的引數獲取 前臺物件 的包名與類名
     *
     * @param packageName 需要檢查是否位於棧頂的App的包名
     */
    public static boolean isForegroundPkgViaDetectionService(String packageName) {
        return packageName.equals(DetectingService.foregroundPackageName);
    }

去設定裡開啟輔助功能,就可以通過isForegroundPkgDetectService( )判斷應用是否在前臺了,只需要傳入相應應用的包為引數即可。

當然,也可以參照以下方式引導使用者開啟輔助功能:

(1)引導使用者開啟輔助功能

 final static String TAG = "AccessibilityUtil";

    // 此方法用來判斷當前應用的輔助功能服務是否開啟
    public static boolean isAccessibilitySettingsOn(Context context) {
        int accessibilityEnabled = 0;
        try {
            accessibilityEnabled = Settings.Secure.getInt(context.getContentResolver(),
                    android.provider.Settings.Secure.ACCESSIBILITY_ENABLED);
        } catch (Settings.SettingNotFoundException e) {
            Log.i(TAG, e.getMessage());
        }

        if (accessibilityEnabled == 1) {
            String services = Settings.Secure.getString(context.getContentResolver(),
                    Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
            if (services != null) {
                return services.toLowerCase().contains(context.getPackageName().toLowerCase());
            }
        }

        return false;
    }

    private void anyMethod() {
        // 判斷輔助功能是否開啟
        if (!isAccessibilitySettingsOn(getContext())) {
            // 引導至輔助功能設定頁面
            startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS));
        } else {
            // 執行輔助功能服務相關操作
        }
    }

效果如下:

5.3 方案特點

1. AccessibilityService有非常廣泛的ROM覆蓋,特別是非國產手機,從API Level 8 (Android2.2)到API Level 23(Android6.0)

2. AccessibilityService不再需要輪詢地判斷當前的應用是不是在前臺,系統會在視窗狀態發生變化的時候主動回撥,耗時和資源消耗都極小;

3.不需要許可權請求;

4.它是一個穩定的方法,並非利用Android一些設計上的 漏洞,可以長期使用的可能很大;

5.可以用來判斷任意應用甚至Activity,PopupWindow,Dialog物件是否處於前臺。

5.4 方案缺點

1、需要使用者手動開啟輔助功能;

2、輔助功能會伴隨應用被“強行停止”或第三方管理工具通過Root而剝奪,而且程序重啟需要對使用者進行重新引導開啟;

3、部分廠商可能對輔助功能進行限制,如已知的vivo部分機型。

6. 自解析/process

6.1 原理

無意中看到烏雲上有人提的一個漏洞,Linux系統核心會把process程序資訊儲存在/proc目錄下,Shell命令去獲取的他,再根據程序的屬性判斷是否為前臺

6.2 優點

  1. 不需要任何許可權
  2. 可以判斷任意一個應用是否在前臺,而不侷限在自身應用

6.3 用法


(1)獲取一系列正在執行的App的程序

List<AndroidAppProcess> processes = ProcessManager.getRunningAppProcesses();

(2)獲取任一正在執行的App程序的詳細資訊

AndroidAppProcess process = processes.get(location);
String processName = process.name;

Stat stat = process.stat();
int pid = stat.getPid();
int parentProcessId = stat.ppid();
long startTime = stat.stime();
int policy = stat.policy();
char state = stat.state();

Statm statm = process.statm();
long totalSizeOfProcess = statm.getSize();
long residentSetSize = statm.getResidentSetSize();

PackageInfo packageInfo = process.getPackageInfo(context, 0);
String appName = packageInfo.applicationInfo.loadLabel(pm).toString();

(3)判斷是否在前臺

if (ProcessManager.isMyProcessInTheForeground()) {
  // do stuff
}

(4)獲取一系列正在執行的App程序的詳細資訊

List<ActivityManager.RunningAppProcessInfo> processes = ProcessManager.getRunningAppProcessInfo(ctx);

6.4 方案特點

1、不需要任何許可權;

2、可以判斷任意一個應用是否在前臺,而不侷限在自身應用;

3、當/proc下資料夾過多時,此方法是耗時操作;

4、該方案存在能耗問題;

5、在Android6.0.1以上版本或部分廠商版本受限於SEAndroid,只能獲取到第3方程序的資訊。

6.5 方案缺點

  1. 當/proc下資料夾過多時,此方法是耗時操作

2.該方案再6.0手機適配執行ok,但在最新的小米、華為6.0.1手機中發現受限於SELinux,無法讀取系統應用的裝置節點進行解析,只能解析第三方應用裝置節點。

6.6 能耗問題解決

1、Java層物件快取:對呼叫比較頻繁的Java層物件在JNI中建立全域性快取,這就避免了每次呼叫時都需要通過JNI介面獲取;

對一些判斷是需要的場景在初始化時由Java層傳入Jni層,並建立全域性快取。

2、過來的為Android程序:將pid小於1000的Native程序過濾掉;

3、只解析發生變化的程序:在每次輪詢解析/proc節點時先判斷程序的pid在快取中是否存在,如果存在只需要更新程序的優先

級資訊,其他資訊不會發生變化;如果程序之前不存在則需要全新解析:

(1)命中快取時的解析程式碼如下

//Code,待補充

(2)未命中快取時,則進行全新解析

//Code,待補充

4、在解析程序時,過來父程序為zygote的程序:Android中所有應用程序的父程序都是Zygote;

5、在Java層對呼叫做快取處理:對於呼叫比較頻繁的情況,如果當次Native呼叫沒有完成,則返回之前的值,不需要阻塞等待;

6、對於只關心前臺程序的場景進行特殊處理:

//code,待補充

通過優化,適配方案的能耗與系統介面基本保持一致。

7.作為系統程序的獲取方式

7.1 技術方案

雖然getRunningTask從Android5.0開始被系統廢棄,但是作為系統應用時,該介面依然是可用的。在使用者取得Root許可權,或者應用跟廠商合作時,應用本身可能會被內建在系統目錄,即:/system/app/或system/private-app/等目錄,因此對於這種情況,使用getRunningTask獲取依然是一種方便的實現。

1、需要判斷應用是否為系統應用:

//Code,待補充。

2、在AndroidManifest.xml中需要宣告如下許可權:

//Code,待補充。

參考連結: