引用出處:
作者: 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;
}
}
```