Android 如何监控当前Foreground Activity,如何获知应用程序的启动

目前主流的监控Foreground Activity的方法,主要有以下三种,各有利弊,还没有找到一个彻底的完美解决方案!

一、使用 AccessibilityService

  • 通过 AccessibilityService可以监控当前Active Window
  • 通过回调函数 onAccessibilityEven 检查事件类型 TYPE_WINDOW_STATE_CHANGED 获知当前 window 改变了
  • 通过调用 PackageManager.getActivityInfo() 检查当前的window 是不是一个Activity

1. 优点:

  • 经过测试Android 2.2 (API 8) ~ Android 5.1.0(API 22) 都可以正常工作,更高版本的还没有测试过
  • 不需要轮询机制,非常省电
  • 不需要申明 GET_TASKS 权限

2. 缺点:

  • 用户必须手动在手机的设置页面开启 Android's accessibility settings 才能收到 AccessibilityEvent事件
  • 当用户手动在手机设置页面关闭 Accessibility setting后,AccessibilityService就会停止运行。你也可以在service 里面用代码 stop itself,但是一旦在代码里面停止service之后,目前还没有找到什么办法能够再次开启。
  • 当用户在设置页面开启 AccessibilityService的时候,有些第三方应用会弹出一个悬浮窗盖住屏幕,导致用户无法点击确认按钮。例如 Velis Auto Brightness 和 Lux 这些App就会干这种事。 目前还不知道如果规避这些第三方应用的恶心行为。
  • AccessibilityService 是被动触发的,无法主动获知当前Activity 的信息,除非当前Activity有所改变

3. 代码示例

public class WindowChangeDetectingService extends AccessibilityService {

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

        //Configure these here for compatibility with API 13 and below.
        AccessibilityServiceInfo config = new AccessibilityServiceInfo();
        config.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
        config.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;

        if (Build.VERSION.SDK_INT >= 16)
            //Just in case this helps
            config.flags = AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;

        setServiceInfo(config);
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
            ComponentName componentName = new ComponentName(
                event.getPackageName().toString(),
                event.getClassName().toString()
            );

            ActivityInfo activityInfo = tryGetActivity(componentName);
            boolean isActivity = activityInfo != null;
            if (isActivity)
                Log.i("CurrentActivity", componentName.flattenToShortString());
        }
    }

    private ActivityInfo tryGetActivity(ComponentName componentName) {
        try {
            return getPackageManager().getActivityInfo(componentName, 0);
        } catch (PackageManager.NameNotFoundException e) {
            return null;
        }
    }

    @Override
    public void onInterrupt() {}
}

4. Merge this into your AndroidManifest.xml

<application>
    <service
        android:label="@string/accessibility_service_name"
        android:name=".WindowChangeDetectingService"
        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/accessibilityservice"/>
    </service>
</application>

5. Put this in res/xml/accessibilityservice.xml:

<?xml version="1.0" encoding="utf-8"?>
<!-- These options MUST be specified here in order for the events to be received on first
 start in Android 4.1.1 -->
<accessibility-service
    xmlns:tools="http://schemas.android.com/tools"
    android:accessibilityEventTypes="typeWindowStateChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagIncludeNotImportantViews"
    android:description="@string/accessibility_service_description"
    xmlns:android="http://schemas.android.com/apk/res/android"
    tools:ignore="UnusedAttribute"/>

6. Enabling the Service

每个用户必须手动到手机设置页面开启 AccessibilityService 这个service才会被调用到。查看具体实现:this StackOverflow answer 

二、使用 ActivityManager

使用 ActivityManager有一定版本限制。官方文档有详细的说明:on API-21 as of LOLLIPOP, ActivityManager.getRunningTasks() is no longer available to third party applications: the introduction of document-centric recents means it can leak person information to the caller. For backwards compatibility, it will still return a small subset of its data: at least the caller's own tasks, and possibly some other tasks such as home that are known to not be sensitive.

网上有人说可以用下面的方法分别调用不同的API,兼容不同的版本:

  • Pre-Lollipop: ActivityManager.getRunningTasks 
  • Lollipop: ActivityManager.getRunningAppProcesses 
在 Android5.0 (API-21)及以上,getRunningTasks将只返回自己和 launcher,getRunningTasks 无法正确判断当前应用是否为front, 不同版本测试的结果也不一样:
在 Android 5.0版本:
  • 1. 当用户在当前App页面时,runningTasksInfos.get(0) = 当前App
  • 2. 当用户回到launch页面时,runningTasksInfos.get(0) = launcher
  • 3. 当用户在其它App页面时,runningTasksInfos.get(0) = launcher
在 Android 5.1, 6.0 以上版本:
  • 1. 当用户在钱盾页面时,runningTasksInfos.get(0) = 当前App
  • 2. 当用户回到launch页面时,runningTasksInfos.get(0) = launcher
  • 3. 当用户在其它App页面时,runningTasksInfos.get(0) = 当前App
对于 getRunningAppProcesses, 经过我自己的测试:
  • 在Android 5.0(API-21) 版本getRunningAppProcesses可以获取所有应用的process,可以通过其来判断top package; 但是在 Android 5.1.0 (API-22)及其以上Android 6.0 (API-23)永远只返回应用自身的process,不能用来判断任意top package,但是可以用来判断当前App应用是否在前台:
  • 官方文档申明 getRunningAppProcesses() 只是用于debugging and management user interfaces,有一定的局限性,说不定哪天就不支持了
  • 依赖反射调用: ActivityManager.RunningAppProcessInfo.processState
  • 无法监控 App switcher activity.

3. 代码示例:

public class CurrentApplicationPackageRetriever {
    private final Context context;
    public CurrentApplicationPackageRetriever(Context context) {
        this.context = context;
    }
    public String[] get() {
        if (Build.VERSION.SDK_INT < 21)
            return getPreLollipop();
        else
            return getLollipop();
    }
    private String[] getPreLollipop() {
        @SuppressWarnings("deprecation")
        List<ActivityManager.RunningTaskInfo> tasks =
            activityManager().getRunningTasks(1);
        ActivityManager.RunningTaskInfo currentTask = tasks.get(0);
        ComponentName currentActivity = currentTask.topActivity;
        return new String[] { currentActivity.getPackageName() };
    }

    private String[] getLollipop() {
        final int PROCESS_STATE_TOP = 2;
        try {
            Field processStateField = ActivityManager.RunningAppProcessInfo.class.getDeclaredField("processState");

            List<ActivityManager.RunningAppProcessInfo> processes =
                activityManager().getRunningAppProcesses();
            for (ActivityManager.RunningAppProcessInfo process : processes) {
                if (
                    // Filters out most non-activity processes
                    process.importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
                    &&
                    // Filters out processes that are just being
                    // _used_ by the process with the activity
                    process.importanceReasonCode == 0
                ) {
                    int state = processStateField.getInt(process);

                    if (state == PROCESS_STATE_TOP)
                        /*
                         If multiple candidate processes can get here,
                         it's most likely that apps are being switched.
                         The first one provided by the OS seems to be
                         the one being switched to, so we stop here.
                         */
                        return process.pkgList; 
                }
            }
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        return new String[] { };
    }

    private ActivityManager activityManager() {
        return (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    }
}

4. 添加 GET_TASKS 权限到 AndroidManifest.xml:

<!--suppress DeprecatedClassUsageInspection -->
<uses-permission android:name="android.permission.GET_TASKS" />

三、通过反射"android.app.ActivityThread"的方式获取Top Activity

public static Activity getRunningActivity() {
    try {
        Class activityThreadClass = Class.forName("android.app.ActivityThread");
        Object activityThread = activityThreadClass.getMethod("currentActivityThread")
                .invoke(null);
        Field activitiesField = activityThreadClass.getDeclaredField("mActivities");
        activitiesField.setAccessible(true);
        ArrayMap activities = (ArrayMap) activitiesField.get(activityThread);
        for (Object activityRecord : activities.values()) {
            Class activityRecordClass = activityRecord.getClass();
            Field pausedField = activityRecordClass.getDeclaredField("paused");
            pausedField.setAccessible(true);
            if (!pausedField.getBoolean(activityRecord)) {
                Field activityField = activityRecordClass.getDeclaredField("activity");
                activityField.setAccessible(true);
                return (Activity) activityField.get(activityRecord);
            }
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }

    throw new RuntimeException("Didn't find the running activity");
}
1. 在有些手机上测试时,发现 It's API > 19,只能获取当前App的Activities
2. 另外在当用“Map” 替换“ArrayMap”发现在4.3的手机可以工作正常. 更高版本的手机还没有测试过

四、通过 UsageStatsManager -> queryUsageStats方法获取Top Activity

调用 queryUsageStats 方法,有一个限制:需要用户到手机的设置页面 Security->Apps 开启Usage的访问权限!

    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
        UsageStatsManager usm = (UsageStatsManager) getSystemService("usagestats");
        long time = System.currentTimeMillis();
        List<UsageStats> appList = usm.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - 1000 * 1000, time);
        if (appList != null && appList.size() > 0) {
            SortedMap<Long, UsageStats> mySortedMap = new TreeMap<Long, UsageStats>();
            for (UsageStats usageStats : appList) {
                mySortedMap.put(usageStats.getLastTimeUsed(), usageStats);
            }
            if (mySortedMap != null && !mySortedMap.isEmpty()) {
                currentApp = mySortedMap.get(mySortedMap.lastKey()).getPackageName();
            }
        }
    } else {
        List<ActivityManager.RunningAppProcessInfo> tasks = am.getRunningAppProcesses();
        currentApp = tasks.get(0).processName;
    }

不要忘记添加permission 到Manifest 文件:

<uses-permission android:name="android.permission.GET_TASKS" /> 
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions" />







  • 5
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值