如何判斷一個 APP 在前臺還是后臺 2024-08-16

引用出處:
作者: wenmingvs
https://blog.csdn.net/yzwfeng/article/details/124584900
Github: https://github.com/wenmingvs/AndroidProcess

方法 原理 需要權(quán)限 可以判斷其他應用位于前臺 特點
方法一 RunningTask Android4可以,5.0以上不行 5.0此方法被廢棄
方法二 RunningProcess 當App存在后臺常駐的Service時失效
方法三 ActivityLifecycleCallbacks 簡單有效,代碼最少
方法四 UsageStatsManager 需要用戶手動授權(quán)
方法五 通過Android無障礙功能實現(xiàn) 需要用戶手動授權(quán)
方法六 讀取/proc目錄下的信息 當proc目錄下文件夾過多時,過多的IO操作會引起耗時
方法七 使用 ProcessLifecycleOwner 監(jiān)聽app的生命周期 使用Jetpack組件
package com.tecsun.self.utils.processutil;

import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.app.Service;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;


import com.tecsun.self.MyApp;
import com.tecsun.self.utils.DetectService;
//import com.tecsun.self.utils.processutil.models.AndroidAppProcess;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * Created by wenmingvs on 2016/1/14.
 */
public class BackgroundUtil {
    public static final int BKGMETHOD_GETRUNNING_TASK = 0;
    public static final int BKGMETHOD_GETRUNNING_PROCESS = 1;
    public static final int BKGMETHOD_GETAPPLICATION_VALUE = 2;
    public static final int BKGMETHOD_GETUSAGESTATS = 3;
    public static final int BKGMETHOD_GETACCESSIBILITYSERVICE = 4;
    public static final int BKGMETHOD_GETLINUXPROCESS = 5;


    /**
     * 自動根據(jù)參數(shù)選擇判斷前后臺的方法
     *
     * @param context     上下文參數(shù)
     * @param packageName 需要檢查是否位于棧頂?shù)腁pp的包名
     * @return
     */
    public static boolean isForeground(Context context, int methodID, String packageName) {
        switch (methodID) {
            case BKGMETHOD_GETRUNNING_TASK:
                return getRunningTask(context, packageName);
            case BKGMETHOD_GETRUNNING_PROCESS:
                return getRunningAppProcesses(context, packageName);
            case BKGMETHOD_GETAPPLICATION_VALUE:
                return getApplicationValue((MyApp) ((Service) context).getApplication());
            case BKGMETHOD_GETUSAGESTATS:
                return queryUsageStats(context, packageName);
            case BKGMETHOD_GETACCESSIBILITYSERVICE:
                return getFromAccessibilityService(context, packageName);
            case BKGMETHOD_GETLINUXPROCESS:
//                return getLinuxCoreInfo(context, packageName);
            default:
                return false;
        }
    }

    /**
     * 方法1:通過getRunningTasks判斷App是否位于前臺,此方法在5.0以上失效
     *
     * @param context     上下文參數(shù)
     * @param packageName 需要檢查是否位于棧頂?shù)腁pp的包名
     * @return
     */
    public static boolean getRunningTask(Context context, String packageName) {
        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        ComponentName cn = am.getRunningTasks(1).get(0).topActivity;
        return !TextUtils.isEmpty(packageName) && packageName.equals(cn.getPackageName());
    }


    /**
     * 方法2:通過runningProcess獲取到一個當前正在運行的進程的List,通過getRunningAppProcesses的IMPORTANCE_FOREGROUND屬性判斷是否位于前臺,當service需要常駐后臺時候,此方法失效 。
     * 缺點:在聊天類型的App中,常常需要常駐后臺來不間斷的獲取服務器的消息,這就需要我們把Service設置成START_STICKY,kill 后會被重啟(等待5秒左右)來保證Service常駐后臺。如果Service設置了這個屬性,這個App的進程就會被判斷是前臺,代碼上的表現(xiàn)就是appProcess.importance的值永遠是 ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND,這樣就永遠無法判斷出到底哪個是前臺了。
     * 在小米 Note上此方法無效,在Nexus上正常
     *
     * @param context     上下文參數(shù)
     * @param packageName 需要檢查是否位于棧頂?shù)腁pp的包名
     * @return
     */
    public static boolean getRunningAppProcesses(Context context, String packageName) {
        ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
        if (appProcesses == null) {
            return false;
        }
        for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
            if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName.equals(packageName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 方法3:通過ActivityLifecycleCallbacks來批量統(tǒng)計Activity的生命周期,來做判斷,此方法在API 14以上均有效,但是需要在Application中注冊此回調(diào)接口
     * 必須:
     * 1. 自定義Application并且注冊ActivityLifecycleCallbacks接口
     * 2. AndroidManifest.xml中更改默認的Application為自定義
     * 3. 當Application因為內(nèi)存不足而被Kill掉時,這個方法仍然能正常使用。雖然全局變量的值會因此丟失,但是再次進入App時候會重新統(tǒng)計一次的
     * 4. 無論用back鍵切到后臺還是用Home鍵切到后臺,都會執(zhí)行onStop,因此都不會影響該方法判斷的正確性
     * @param myApplication
     * @return
     */

    public static boolean getApplicationValue(MyApp myApplication) {
        return myApplication.getAppCount() > 0;
    }

    /**
     * 方法4:通過使用UsageStatsManager獲取,此方法是ndroid5.0A之后提供的API
     * 必須:
     * 1. 此方法只在android5.0以上有效
     * 2. AndroidManifest中加入此權(quán)限<uses-permission xmlns:tools="http://schemas.android.com/tools" android:name="android.permission.PACKAGE_USAGE_STATS"
     * tools:ignore="ProtectedPermissions" />
     * 3. 打開手機設置,點擊安全-高級,在有權(quán)查看使用情況的應用中,為這個App打上勾
     *
     * @param context     上下文參數(shù)
     * @param packageName 需要檢查是否位于棧頂?shù)腁pp的包名
     * @return
     */

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public static boolean queryUsageStats(Context context, String packageName) {
        class RecentUseComparator implements Comparator<UsageStats> {
            @Override
            public int compare(UsageStats lhs, UsageStats rhs) {
                return (lhs.getLastTimeUsed() > rhs.getLastTimeUsed()) ? -1 : (lhs.getLastTimeUsed() == rhs.getLastTimeUsed()) ? 0 : 1;
            }
        }
        RecentUseComparator mRecentComp = new RecentUseComparator();
        long ts = System.currentTimeMillis();
        UsageStatsManager mUsageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
        List<UsageStats> usageStats = mUsageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_BEST, ts - 1000 * 10, ts);
        if (usageStats == null || usageStats.size() == 0) {
            if (HavaPermissionForTest(context) == false) {
                Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                context.startActivity(intent);
                Toast.makeText(context, "權(quán)限不夠\n請打開手機設置,點擊安全-高級,在有權(quán)查看使用情況的應用中,為這個App打上勾", Toast.LENGTH_SHORT).show();
            }
            return false;
        }
        Collections.sort(usageStats, mRecentComp);
        String currentTopPackage = usageStats.get(0).getPackageName();
        if (currentTopPackage.equals(packageName)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 判斷是否有用權(quán)限
     * @param context 上下文參數(shù)
     */
    @TargetApi(Build.VERSION_CODES.KITKAT)
    private static boolean HavaPermissionForTest(Context context) {
        try {
            PackageManager packageManager = context.getPackageManager();
            ApplicationInfo applicationInfo = packageManager.getApplicationInfo(context.getPackageName(), 0);
            AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
            int mode = appOpsManager.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, applicationInfo.uid, applicationInfo.packageName);
            return (mode == AppOpsManager.MODE_ALLOWED);
        } catch (PackageManager.NameNotFoundException e) {
            return true;
        }
    }

    /**
     * 方法5:通過Android自帶的無障礙功能,監(jiān)控窗口焦點的變化,進而拿到當前焦點窗口對應的包名
     * 必須:
     * 1. 創(chuàng)建ACCESSIBILITY SERVICE INFO 屬性文件
     * 2. 注冊 DETECTION SERVICE 到 ANDROIDMANIFEST.XML
     *  優(yōu)點:
      *    1、AccessibilityService 不再需要輪詢的判斷當前的應用是不是在前臺,系統(tǒng)會在窗口狀態(tài)發(fā)生變化的時候主動回調(diào),耗時和資源消耗都極小。
      *    2、不需要權(quán)限請求
      *    3、它是一個穩(wěn)定的方法,與 “方法6”讀取 /proc 目錄不同,它并非利用 Android 一些設計上的漏洞,可以長期使用的可能很大。
   * 4、可以用來判斷任意應用甚至 Activity, PopupWindow, Dialog 對象是否處于前臺
* 缺點:1、需要要用戶開啟輔助功能。2、輔助功能會伴隨應用被“強行停止”而剝奪
     * @param context
     * @param packageName
     * @return
     */
    public static boolean getFromAccessibilityService(Context context, String packageName) {
        if (DetectService.isAccessibilitySettingsOn(context) == true) {
            DetectService detectService = DetectService.getInstance();
            String foreground = detectService.getForegroundPackage();
            Log.d("wenming", "**方法五** 當前窗口焦點對應的包名為: =" + foreground);
            return packageName.equals(foreground);
        } else {
            Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(intent);
            Toast.makeText(context, "accessbiliityNo", Toast.LENGTH_SHORT).show();
            return false;
        }
    }

    /**
     * 方法6:無意中看到烏云上有人提的一個漏洞,Linux系統(tǒng)內(nèi)核會把process進程信息保存在/proc目錄下,使用Shell命令去獲取的他,再根據(jù)進程的屬性判斷是否為前臺。
    *  優(yōu)點:1、不需要任何權(quán)限。2、可以判斷任意一個應用是否在前臺,而不局限在自身應用。
    *  缺點:當/proc下文件夾過多時,此方法是耗時操作。
     *
     * @param packageName 需要檢查是否位于棧頂?shù)腁pp的包名
     */
//    public static boolean getLinuxCoreInfo(Context context, String packageName) {
//
//        List<AndroidAppProcess> processes = ProcessManager.getRunningForegroundApps(context);
//        for (AndroidAppProcess appProcess : processes) {
//            if (appProcess.getPackageName().equals(packageName) && appProcess.foreground) {
//                return true;
//            }
//        }
//        return false;
//
//    }


}

DetectService代碼如下:

package com.tecsun.self.utils;

import android.accessibilityservice.AccessibilityService;
import android.content.Context;
import android.provider.Settings;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;

/**
 * Created by wenmingvs on 16/2/10.
 */
public class DetectService extends AccessibilityService {

    private static String mForegroundPackageName;
    private static DetectService mInstance = null;

    public DetectService() {
    }

    public static DetectService getInstance() {
        if (mInstance == null) {
            synchronized (DetectService.class) {
                if (mInstance == null) {
                    mInstance = new DetectService();
                }
            }
        }
        return mInstance;
    }

    /**
     * 監(jiān)聽窗口焦點,并且獲取焦點窗口的包名
     *
     * @param event
     */
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
            mForegroundPackageName = event.getPackageName().toString();
        }
    }

    @Override
    public void onInterrupt() {
    }

    public String getForegroundPackage() {
        return mForegroundPackageName;
    }


    /**
     * 此方法用來判斷當前應用的輔助功能服務是否開啟
     *
     * @param context
     * @return
     */
    public static boolean isAccessibilitySettingsOn(Context context) {
        int accessibilityEnabled = 0;
        try {
            accessibilityEnabled = Settings.Secure.getInt(context.getContentResolver(),
                    Settings.Secure.ACCESSIBILITY_ENABLED);
        } catch (Settings.SettingNotFoundException e) {
            Log.d("wenming", 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;
    }
}
```
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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