判断应用程序在前台、后台的方法。

简介

工程链接:AndoridProcess

方法 原理 需要权限 其他应用是否有效 特点
方法一 RunningTask Andorid 5.0 之后只能获取自身信息 5.0之后废除
方法二 RunningProcess App存在后台常驻的Service时失效
方法三 ActivityLifecycleCallbaks 简单有效
方法四 UsageStatsManager 需要用户手动授权
方法五 通过Android无障碍功能实现 需要用户手动授权
方法六 读取Linux内核下/proc目录信息 一些版本没有系统级应用信息,一些 OEM 无效,黑科技。

通过 getRunningTasks 判断 App 是否位于前台, 此方法在5.0以上失效。

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

ActivityManager#getRunningTasks
List<ActivityManager.RunningTaskInfo> getRunningTasks (int maxNum)
I
注意:

该方法在API level 21之后已弃用。
在LOLLIPOP中,这个方法已不再允许第三方应用使用:它可能向调用者泄露用户隐私信息。为了兼容性,该方法依然会返回一小部分数据,至少会有调用者自己的任务,有可能会包含一些其他的例如桌面这种不太敏感的数据。

介绍:
返回一组最近运行任务的列表,按时间排序,最近使用排在最前。注意“运行”并不代表该任务的任何代码或者界面正在运行当中——它们可能被系统冻结,日后也许会重启到之前的状态,恢复至前端。

注意:该方法仅准备为调试和任务展示的用户接口使用该方法不应作为应用的核心功能,例如通过这里的信息来决定不同的行为之类。这种的使用方式是不支持的,而且很有可能在未来不生效。例如,若支持多应用同时允许的情况下,该方法返回值就不正确。

相关介绍:

axNum: int, 返回数据列表的大小。实际的返回数量也许会更小,这取决于用户运行过的任务数量。

ActivityManager.RunningTaskInfo: 某个于系统中刚刚“运行”的任务所能得到的信息。注意运行中的任务并不代表它正在前台运行当中,它仅仅表示用户曾经运行过并且没有关掉,但是系统可能杀掉该进程来释放内存,仅保留退出状态的一些信息以便恢复。

字段类型 字段描述
public static final Creator<ActivityManager.RunningTaskInfo> CREATOR
public ComponentName baseActivity 任务中执行的第一个activity组件
public CharSequence description 任务当前状态的描述
public int id 任务的唯一标识符
public int numActivities 任务中activity的数量
public int public int 任务中当前正在运行的activity的数量(不停止,持续)
public Bitmap thumbnail 表示任务当前状态的缩略图
public ComponentName topActivity 任务栈中顶部的activity组件

通过 getRunningAppProcesses 的 IMPORTANCE_FOREGROUND 属性判断是否位于前台,当 service 需要常驻后台时候,此方法在小米 Note 上此方法无效,在 Nexus 上正常

public static boolean getRunningAppProcesses(Context context, String packageName) {
    ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERICE);
    List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
    if (appProcess == null) {
        return false;
    }
    for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
        if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FORGROUND &&
            appProcess.processName.equals(packageName)) {
                return true;
            }
    }
    return false;
}

ActivityManager#getRunningAppProcesses
List<ActivityManager.RunningAppProcessInfo> getRunningAppProcesses()

介绍:

返回一组在设备上运行的应用进程信息列表。顺序不定。

注意:该方法仅用于测试或构建一个面向用户的进程管理UI

相关介绍:

ActivityManager.RunningAppProcessInfo
可以获得的运行中进程信息。

常量

类型 名称 描述
int IMPORTANCE_BACKGROUND importance 常量:该进程包含后台运行代码
int IMPORTANCE_mZONE importance 常量:该进程不存在
int IMPORTANCE_PERCEPTIBLE importance 常量:该进程并不是用户可以直接感知到的,而是从某种角度、某种程度可见的
int IMPORTANCE_SERVICE importance 常量:该进程包含持续运行的 service
int IMPORTANCE_TOP_SLEEPING importance 常量:该进程在最前端的 UI 展示,但是设备正在睡眠中所以对用户不可见
int IMPORTANCE_VISIBLE importance 常量:该进程包含确实对用户可见的内容,即使不是在当前的最前端
int REASON_PROVIDER_IN_USE importanceReasonCode 常量:应用的某一个 content provider 正在被其他进程使用
int REASON_SERVICE_IN_USE importanceReasonCode 常量:应用的某一个 content provider 正在被其他进程使用
int REASON_UNKNOWN importanceReasonCode 常量:这个等级没什么特别的原因

变量

类型 名称 描述
public static final Creator<ActivityManager.RUnningAppProcessInfo> CREATOR nothing
public int importance 系统为该进程赋予的相对重要等级
public int importanceReasonCode 进程重要等级的原因,如果有的话
public ComponentName importanceReasonComponent 对于特定值的importanceReasonCode,表示正在被使用的组件名称
public int importanceReasonPid 对于特定值的值的importanceReasonCode,表示访问本进程的进程 ID
public int lastTrimLevel 上一次内存释放报告给进程的进程等级,依赖方法ComponentCallbacks2.onTrimMemory(int)提供
public int lru 额外增加的importance特征排序信息,提供本进程LRU(Leaset Recently Used)优先级
public int pid 本进程 ID。空为0
public String[] pkgList 加载到本进程内的所有包名
public String processName 本进程的名称
public int uid 本进程的用户id

通过 ActivityLifecycleCallbacks 来批量统计 Activity 的生命周期,用此作为判断,在API 14 以上均有效,需要在 Application 中注册此回调接口。

条件:

  1. 自定义 Application 并注册 ActivityLifecycleCallbacks 接口。
  2. 于 AndroidManifest.xml 中修改默认 Application 为自定义。
  3. 当 Application 因内存不足而被 kill 时,此方法依然有效。虽然全局变量的值会因此丢失,但是再次进入 App 后会重新统计一次。
public class MyApplication extends Application {
    private int appCount = 0;
    @Override
    public void onCreate() {
        super.onCreate();
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {

            }

            @Override
            public void onActivityStarted(Activity activity) {
                appCount++;
            }

            @Override             
            public void onActivityResumed(Activity activity) {

            }

            @Override
            public void onActivityPaused(Activity activity) {

            }

            @Override
            public void onActivityStopped(Activity activity) {
                appCount--;
            }

            @Override
            public void onActivitySaveInstanceState(Activity activity) {

            }

            @Override
            public void onActivityDestroyed(Activity activity) {

            }
        });
    }

    public int getAppCount() {
        return appCount;
    }
}

public static boolean isApplicationForground(MyApplication myApplication) {
    return myApplication.getAppCount() > 0;
}

通过使用UsageStatsManager获取,此方法为 Android5.0 之后提供的API

条件:

  1. 仅在 android 5.0 以上有效
  2. 于 AndroidManifest.xml 中加入 <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
  3. 打开手机设置,点击安全-高级-有权查看使用情况的应用,选择开启这个应用
@TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
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 (!havePermissionForTest(context)) {
            Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(intent);
            Toast.makeText(context, "权限不够\n请打开手机设置,点击安全-高级-有权查看使用情况的应用,开启这个应用的权限",Toast.LENGTH_LONG).show();
        }
        return false;
    }
    Collections.sort(usageStats, mRecentComp);
    String currentTopPackage = usageStats.get(0).getPackageName();
    return currentTopPackage.equals(packageName);
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static boolean havePermissionForTest(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_STAGS, applicationInfo.uid, applicationInfo.packageName);
        return mode == AppOpsManager.MODE_ALLOWED;
    } catch (PackageManager.NameNotFoundException e) {
        return true;
    }
}

相关介绍:

UsageStatsManager

提供设备使用历史和统计数据。使用数据是按照时间划分的:日,周,月和年。当请求某特定时间的使用数据时,请求类似下面这种:

PAST                   REQUEST_TIME                    TODAY                   FUTURE  
 ————————————————————————————||———————————————————————————¦-----------------------|
                        YEAR ||                           ¦                       |
 ————————————————————————————||———————————————————————————¦-----------------------|
  MONTH            |         ||                MONTH      ¦                       |
 ——————————————————|—————————||———————————————————————————¦-----------------------|
   |      WEEK     |     WEEK||    |     WEEK     |     WE¦EK     |      WEEK     |
 ————————————————————————————||———————————————————|———————¦-----------------------|
                             ||           |DAY|DAY|DAY|DAY¦DAY|DAY|DAY|DAY|DAY|DAY|
 ————————————————————————————||———————————————————————————¦-----------------------|

上图是包含了请求时间间隔的一段请求时间间隔。(好绕)

注意:本 API 需要权限 android_permission.PACKAGE_USAGE_STATS,是系统级权限,并且不会授予第三方应用。然而声明权限可以让用户在设置中为应用手动授予该权限。

常量

类型 名称 描述
int INTERVAL_BEST 一种时间间隔类型,使用给定时间段的最适合时间间隔
int INTERVAL_DAILY 一种跨越日的时间间隔类型
int INTERVAL_MONTHLY 一种跨越月的时间间隔类型
int INTERVAL_WEEKLY 一种跨越周的时间间隔类型
int INTERVAL_YEAR 一种跨越年的时间间隔类型

公共方法

类型 名称 描述
boolean isAppInactive(String packageName) 返回特定的应用当前是否处于闲置状态
Map<String, UsageStats> queryAndAggregateUsageStats(long beginTime, long endTime) 一个便捷方法,获取给定时间段内所有状态的(使用INTERVAL_BEST)应用,集成在一个数据集合中,按包名排序
List<ConfigurationStats> aueryConfigurations(int intervalType, long beginTime, long endTime) 获取设备一段时间间隔内的硬件设置信息
UsageEvents queryEvents(long beginTime, long endTime) 给定时间间隔内的时间
List<UsageStats> queryUsageStats(int intervalType, long beginTime, long endTime) 获取一段时间内、特定时间间隔类型的应用使用信息

UsageStats

包含了应用包的一段特定时间内的使用统计信息。

变量

类型 名称 描述
public static final Creator<UsageStats> CREATOR nothing

方法

类型 名称 描述
void add(UsageStats right) 添加统计,从右至左
int describeContents() 描述这个 Parcelable 对象中包含的特殊对象类型
long getFirstTimeStamp() 获取这个 UsageStats 描述的时间段的开始时间戳
long getLastTimeStamp() 获取这个 UsageStats 描述的时间段的结束时间戳
long getLastTimeUsed() 获取上一次这个包被使用的时间戳
String getPackageName() 获取包名
long getTotalTimeInForeground() 获取此包在前台的时间总长,毫秒级

ConfigurationStats

描述设备 Configuration 的一段时间内使用统计数据。

方法

类型 名称 描述
int getActivationCount() 获取这个配置激活的次数
long getFirstTimeStamp() 获取时间段的开始时间戳
long getLastTimeActive() 获取上一次激活的时间戳
long getLastTimeStamp() 获取时间段结束的时间戳
long getTotalTimeActive() 获取总共的激活时间

通过 Android 自带无障碍功能,监控窗口焦点的变化,拿到焦点窗口对应包名

条件:

  1. 创建 ACCESSIBILITY SERVICE INFO 属性文件
  2. 注册 DETECTION SERVICE 到 AndroidManifest.xml
public static boolean getFromAccessibilityService(Context context, String packageName) {
    if (DetectService.isAccessibilitySettingsOn(context)) {
        DetectService detectService = DetectService.getInstance();
        String foreground = detectService.getForegroundPackage();
        Log.d(DEBUG, "当前窗口焦点对应包名为:" + foreground);
        return packageName.equals(foreground);
    } else {
        Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY+SETTINGS);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
        Toask.makeTexxt(context, "请为App打开辅助功能开关", Toast.LENGTH_SHORT).show();
        return false;
    }
}

public class DetectService extends AccessibilityService {
    
    private static String mForegroundPackageName;
    private static DetectService mInstatnce = null;

    private DetectService {}

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

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
            mForegroundPackageName = event.getPackageName().toString();
        }
    }
    
    @Override
    public void onInterrupt() {
    }
    
    public String getForegroundPackageName() {
        return mForegroundPackageName;
    }
    
    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.d(DEBUG, e.getMessage());
        }

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

相关介绍:

Developing an Accessibility Service
AccessibilityService API
Settings
Settings API

Linux 系统内核会把 process 进程信息保存在 /proc 目录下,使用 Shell 命令去获取,再根据进程属性判断是否为前台

public static boolean getLinuxCoreInfo(Context context, String packageName) {
    List<AndroidAppProcess> processes = ProcessManager.getRunningForegroundApps(context);
    for (AndroidAppProcess appProcess : processes) {
        if (appProcesse.getPackageName().euqals(packageName) && appProcess.foreground) {
            return true;
        }
    }
    return false;
}

优点:

  1. 不需要任何权限。
  2. 可以判断任意一个应用是否在前台,不局限自身。

缺点:

  1. 当 /proc 下文件较多时,此方法耗时。
  2. 一些 Android 版本中系统级应用并不能显示,因为它们包含更高级的 SELinux 环境。
  3. 不能完全取代 getRunningAppProcesses()。这个库也不能提供进程的pkgListlruimportance
  4. 这个方法目前在 N 的开发预览版中不支持。

相关介绍:

对应工程在此处:AndroidProcesses,这个库的主要目的是为了提供一种自5.0之后的获取运行中应用的方法。
由前面的介绍可知,5.0之前可以使用 ActivityManager#getRunningTasks(int)ActivityManager#getRunningAppProcesses() 两个方法来获取正在运行的进程信息,
而判断程序在前端后端的方法一、二也正对应了这两个方法的应用。在5.0之后为了保护用户隐私,这两个方法都被废除,能做的事也被大幅度削弱。
当然也可以采用第四种方案,使用 UsageStatsManager 来获取使用信息,但是这需要保证应用权限的开启。还有一些 OEM 并不支持这个设定。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,834评论 25 707
  • 如何能让我们的应用能够在系统后台持续地运行是一个自Android从娘胎里出来时就议论不停的话题,而且这似乎成了一个...
    骏骏的简书阅读 1,107评论 1 19
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 和新昌来场说走就走的初夏约会,也是佛缘。新昌隶属绍兴,跟着课本游百草园,学孔乙己喝碗温热黄酒,看看鲁迅故居,也许是...
    冷酷的猫阅读 301评论 0 1
  • 忘记了母亲的乳汁是多么甜美, 但我始终记得母亲的微笑, 阳光般地照彻朦胧的黑夜, 一双眼睛, 默默地看着我走向远方...
    悦者阅读 394评论 1 6