Android:如何从服务中获取当前前台活动(foreground activity)?

198

有没有一种原生的Android方法可以从服务中获取对当前正在运行的Activity的引用?

我有一个在后台运行的服务,当事件发生(在服务中)时,我想更新我的当前活动。有没有一种简单的方法可以做到这一点(就像我上面提出的那样)?


也许这可以给你一些想法 http://stackoverflow.com/a/28423385/185022 - AZ_
13个回答

148

更新:从Android 5.0开始,这个不再适用于其他应用程序的活动。


这里有一个使用活动管理器的好方法。你可以从活动管理器中获取runningTasks,它总是会首先返回当前活动的任务。然后你可以从中获取topActivity。 示例在这里 从ActivityManager服务中获取正在运行任务列表的方法很简单。你可以请求手机上运行的任务的最大数量,默认情况下,当前活动的任务会首先返回。
一旦获取到,你就可以通过请求列表中的topActivity来获取ComponentName对象。
以下是一个示例。
    ActivityManager am = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    List<ActivityManager.RunningTaskInfo> taskInfo = am.getRunningTasks(1);
    Log.d("topActivity", "CURRENT Activity ::" + taskInfo.get(0).topActivity.getClassName());
    ComponentName componentInfo = taskInfo.get(0).topActivity;
    componentInfo.getPackageName();

您需要在清单文件中添加以下权限:

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

3
你的答案是100%正确的。谢谢你。最好你在这里发布相同的答案。 - Shahzad Imam
2
如果我需要更频繁地获取当前活动,那么我必须非常频繁地轮询,对吗?有没有一种方法可以在前台活动更改时获得回调事件? - nizam.sp
43
提醒大家注意,关于getRunningTasks()方法,文档中注明了以下内容:注意:此方法仅用于调试和展示任务管理用户界面。在应用程序的核心逻辑中,比如根据此处获取的信息决定不同行为,永远不应使用该方法。这些用法不被支持,并且将来可能会出现问题。例如,如果多个应用程序可以同时运行,则基于此处数据的控制流目的的假设将是不正确的。 - Ryhan
30
自LOLLIPOP开始,此方法不再向第三方应用程序开放:由于引入基于文档的最近列表,调用者可能会泄露个人信息。为了保持向后兼容性,该方法仍将返回其数据的一个小子集:至少是调用者自己的任务,可能还包括一些已知不敏感的任务,如主屏幕。 - sherpya
2
有没有办法在Lollipop中获取顶部活动? - aka_007
显示剩余6条评论

128

警告:Google Play违规

Google已经威胁从Play商店中删除那些将辅助功能服务用于非辅助目的的应用程序。但是,据报道,这一政策正在重新考虑。


使用AccessibilityService

优点

  • 在Android 2.2 (API 8)到7.1 (API 25)版本中测试并可用。
  • 不需要轮询。
  • 不需要GET_TASKS权限。

缺点

  • 每个用户都必须在Android的辅助功能设置中启用该服务。
  • 这不是100%可靠的。有时事件顺序可能错乱。
  • 该服务始终在运行。
  • 当用户尝试启用AccessibilityService时,如果某个应用程序将覆盖屏幕,则无法按下“确认”按钮。一些这样做的应用程序包括 Velis Auto Brightness 和 Lux。这可能会让用户感到困惑,因为他们可能不知道为什么无法按下按钮或如何解决。
  • AccessibilityService直到第一个活动更改之前,都不会知道当前活动窗口。

示例

服务

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) {
            if (event.getPackageName() != null && event.getClassName() != null) {
                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() {}
}

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>

服务信息

将以下内容放置在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"/>

启用服务

每个应用的用户都需要明确启用辅助功能服务,以便使用它。有关如何执行此操作,请参见此 StackOverflow 答案

请注意,如果应用程序在屏幕上放置了叠加层(例如 Velis Auto Brightness 或 Lux),则用户在尝试启用辅助功能服务时将无法按下“确定”按钮。


1
@lovemint,我的意思是:我指定了一个例子settingsActivity,它是your.app.ServiceSettingsActivity,所以你应该将其更改为你自己的辅助功能服务设置活动。我认为设置活动是可选的,所以我从我的答案中删除了这部分,使它更简单。 - Sam
1
非常感谢,只有一个最后的问题。如果我需要处理从API 8到当前版本,我应该在代码中完成这项工作,而不是使用XML吗? - paolo2988
1
@Sam 我可以确认这个服务完美地工作。有一个奇怪的行为,我在主活动中使用了一个意图来启用辅助功能服务。在第一次激活后,服务被激活,但 onAccessibilityEvent 没有接收到任何事件,但是如果我禁用并再次启用辅助功能服务,则服务会重新激活,并且 onAccessibilityEvent 开始工作。 - paolo2988
1
@lovemint,感谢您发现这个问题!看起来是Android 4.1.1中的一个错误。我通过在XML文件和代码中指定辅助服务配置来修复了它。请参见更新后的答案以获取示例。 - Sam
2
感谢您的代码。我为您创建了这个应用程序 :) - vault
显示剩余22条评论

88

有没有一种原生的Android方法可以从服务中获取对当前正在运行的Activity的引用?

您可能不拥有“当前正在运行的Activity”。

我有一个在后台运行的服务,并且当发生事件(在服务中)时,我想更新我的当前Activity。有没有简单的方法可以做到这一点(就像我上面提到的那样)?

  1. 向Activity发送广播Intent -- 这里是演示此模式的示例项目
  2. 让Activity提供一个PendingIntent(例如通过createPendingResult()方法),由服务调用
  3. 通过bindService()方法将回调或监听对象注册到服务中,并让服务调用该回调/监听对象的事件方法
  4. 向Activity发送有序广播Intent,并使用低优先级的BroadcastReceiver作为备份(如果Activity不在屏幕上,则会引发通知)-- 这里是有关此模式的博客文章

3
谢谢,但由于我有很多活动,并且不想更新它们所有,所以我正在寻找一种安卓方式来获取前台活动。正如你所说,我应该无法这样做,这意味着我必须想办法绕过这个限制。 - George
1
@George:无论如何,你想要的都不会对你有所帮助。正如你所指出的,你有“很多活动”。因此,它们都是单独的类。因此,如果没有大量的instanceof检查或重构活动以共享公共超类或接口,您的服务将无法处理它们。如果您要重构这些活动,最好以更适合框架并涵盖更多场景的方式进行,例如没有任何活动处于活动状态。第4种方法可能是最少工作和最灵活的。 - CommonsWare
2
谢谢,但我有更好的解决方案。我的所有活动都扩展了一个自定义的BaseActivity类。我设置了一个ContextRegister,每当它在前台时将该活动注册为当前活动,只需在我的BaseActivity类中使用3行代码即可。仍然感谢您的支持。 - George
3
@George:要注意内存泄漏问题。在Java中,尽可能避免使用可变的静态数据成员。 - CommonsWare
我已经严密地封装了对象,但我确实会记住这一点。谢谢。 - George

27

可以通过以下方式完成:

  1. 实现自己的应用程序类,注册ActivityLifecycleCallbacks - 这样您就可以看到我们的应用程序正在进行的情况。 在每次恢复时,回调将当前可见的活动分配给屏幕,并在暂停时删除该分配。 它使用API 14中添加的方法registerActivityLifecycleCallbacks()

    public class App extends Application {
    
    private Activity activeActivity;
    
    @Override
    public void onCreate() {
        super.onCreate();
        setupActivityListener();
    }
    
    private void setupActivityListener() {
    registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
            }
            @Override
            public void onActivityStarted(Activity activity) {
            }
            @Override
            public void onActivityResumed(Activity activity) {
                activeActivity = activity;
            }
            @Override
            public void onActivityPaused(Activity activity) {
                activeActivity = null;
            }
            @Override
            public void onActivityStopped(Activity activity) {
            }
            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
            }
            @Override
            public void onActivityDestroyed(Activity activity) {
            }
        });
    }
    
    public Activity getActiveActivity(){
        return activeActivity;
    }
    
    }
    
  2. 在你的服务中调用getApplication()并将其转换为你的应用程序类名称(在这种情况下为App)。然后,您可以调用app.getActiveActivity() - 这将为您提供当前可见的活动(或者当没有活动可见时返回null)。您可以通过调用activeActivity.getClass().getSimpleName()来获取活动的名称。


1
我在 activeActivity.getClass().getSimpleName() 处遇到了 Nullpointer 异常,请问你能帮忙吗? - beginner
正如您从ActivityLifecycleCallbacks中所看到的那样 - 如果活动不可见,application.getActiveActivity()将返回null。这意味着没有活动是可见的。您需要在服务或其他任何使用此功能的地方进行检查。 - Vit Veres
好的,但是如果活动恢复,则不应返回null吧?我的活动已经启动,在其onResume中我得到了这样的结果。 - beginner
很难猜测,尝试在onActivityResumed()onActivityPaused()getActiveActivity()处设置断点,以查看如何调用它们(如果有的话)。 - Vit Veres
2
作为初学者,在将activeActivity赋值为null之前,必须检查activityactiveActivity是否相同,以避免由于各种活动的生命周期方法调用顺序混乱而导致错误。 - Rahul Tiwari
这种方法很好,但是如果您在后台堆栈中有两个活动,并且更改了设备方向,则getActiveActivity()将始终返回后台的活动,而不是可见的活动。有人知道如何解决这个问题吗? - Mohamed Zakaria El-Zoghbi

18

我们团队找不到满意的解决方案,所以我们自己开发了一个。我们使用ActivityLifecycleCallbacks来跟踪当前活动并通过服务公开它:

public interface ContextProvider {
    Context getActivityContext();
}

public class MyApplication extends Application implements ContextProvider {
    private Activity currentActivity;

    @Override
    public Context getActivityContext() {
         return currentActivity;
    }

    @Override
    public void onCreate() {
        super.onCreate();

        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                MyApplication.this.currentActivity = activity;
            }

            @Override
            public void onActivityStarted(Activity activity) {
                MyApplication.this.currentActivity = activity;
            }

            @Override
            public void onActivityResumed(Activity activity) {
                MyApplication.this.currentActivity = activity;
            }

            @Override
            public void onActivityPaused(Activity activity) {
                MyApplication.this.currentActivity = null;
            }

            @Override
            public void onActivityStopped(Activity activity) {
                // don't clear current activity because activity may get stopped after
                // the new activity is resumed
            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

            }

            @Override
            public void onActivityDestroyed(Activity activity) {
                // don't clear current activity because activity may get destroyed after
                // the new activity is resumed
            }
        });
    }
}

然后配置您的 DI 容器,使其返回 MyApplication 的实例,以供 ContextProvider 使用,例如:

public class ApplicationModule extends AbstractModule {    
    @Provides
    ContextProvider provideMainActivity() {
        return MyApplication.getCurrent();
    }
}

(请注意,上面的代码中省略了getCurrent()的实现。这只是一个从应用程序构造函数设置的静态变量。)

6
在将currentActivity赋值为null之前,必须检查activitycurrentActivity是否相同,以避免由于各种活动的生命周期方法调用顺序混杂而导致的错误。 - Rahul Tiwari

15

使用ActivityManager

如果您只想知道包含当前活动的应用程序,可以使用ActivityManager实现。可使用的技术取决于Android版本:

优点

  • 在迄今为止的所有Android版本中都应该工作。

缺点

  • 在Android 5.1+中不起作用(它只会返回您自己的应用程序)
  • 这些API的文档表示,它们仅供调试和管理用户界面使用。
  • 如果您想要实时更新,需要使用轮询。
  • 依赖于隐藏API:ActivityManager.RunningAppProcessInfo.processState
  • 此实现无法获取应用程序切换器活动。

示例(基于KNaito's code

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 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) {
                        String[] processNameParts = process.processName.split(":");
                        String packageName = processNameParts[0];

                        /*
                         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 packageName;
                    }
                }
            }
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }

        return null;
    }

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

}

清单文件

AndroidManifest.xml中添加GET_TASKS权限:

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

11
这个在 Android M 上不再起作用了。getRunningAppProcesses() 现在只返回你应用的包名。 - Lior Iluz
1
同样也不支持 API Level 22(Android 5.1)。在 Build LPB23 上进行了测试。 - sumitb.mdi

10

我正在使用这个进行测试。它适用于API > 19,但仅适用于您应用程序中的活动。

@TargetApi(Build.VERSION_CODES.KITKAT)
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
将ArrayMap替换为Map,它也可以在4.3上运行。未在早期的Android版本中进行测试。 - Oliver Jonas

4

以下是我建议的做法,也是我验证有效的方式。在你的Application类中,实现一个Application.ActivityLifeCycleCallbacks监听器,并在你的Application类中设置一个变量。然后根据需要查询该变量。

class YourApplication: Application.ActivityLifeCycleCallbacks {
    
    var currentActivity: Activity? = null

    fun onCreate() {
        registerActivityLifecycleCallbacks(this)
    }

    ...
    
    override fun onActivityResumed(activity: Activity) {
        currentActivity = activity
    }
}

2

在API 21或以上版本中使用此代码。相比其他答案,这段代码能够提供更好的结果,它能够完美地检测前台进程。

if (Build.VERSION.SDK_INT >= 21) {
    String currentApp = null;
    UsageStatsManager usm = (UsageStatsManager) this.getSystemService(Context.USAGE_STATS_SERVICE);
    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();
        }
    }

你能否详细说明一下,这个方法的结果为什么比其他答案更好? - Sam
这也适用于通知菜单吗?我遇到了一个问题,如果我接到电话或收到短信,UsageStatsManager会开始给我系统UI的包名而不是我的应用程序。 - kukroid
@kukroid,请发布一个单独的问题,并包括您的源代码、设备信息以及您正在使用的消息和电话应用程序。 - Sam

2

我喜欢Application.ActivityLifecycleCallbacks的想法。但是获取顶部活动可能有点棘手。

  • 您的应用程序可能有多个活动
  • 一些活动可能会被销毁
  • 一些活动可能会进入后台

为了处理所有这些情况,您需要跟踪每个活动的生命周期。这正是我使用下面的解决方案所做的。

所有内容都汇总到一个单独的调用getTopForegroundActivity()中,该调用返回顶部前台活动或null(如果堆栈中没有活动或其中没有活动在前台)。

用法

public class MyApp extends Application {

    private ActivityTracker activityTracker;

    @Override
    public void onCreate() {
        super.onCreate();

        activityTracker = new ActivityTracker();
        registerActivityLifecycleCallbacks(activityTracker);
        
        ...

        Activity activity = activityTracker.getTopForegroundActivity();
        if(activity != null) {
            // Do something
        }
    }
}

来源

public class ActivityTracker implements Application.ActivityLifecycleCallbacks {
    private final Map<Activity, ActivityData> activities = new HashMap<>();


    public Activity getTopForegroundActivity() {
        if (activities.isEmpty()) {
            return null;
        }
        ArrayList<ActivityData> list = new ArrayList<>(activities.values());
        Collections.sort(list, (o1, o2) -> {
            int compare = Long.compare(o2.started, o1.started);
            return compare != 0 ? compare : Long.compare(o2.resumed, o1.resumed);
        });

        ActivityData topActivity = list.get(0);
        return topActivity.started != -1 ? topActivity.activity : null;
    }


    @Override
    public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
        activities.put(activity, new ActivityData(activity));
    }

    @Override
    public void onActivityStarted(@NonNull Activity activity) {
        ActivityData activityData = activities.get(activity);
        if (activityData != null) {
            activityData.started = System.currentTimeMillis();
        }
    }

    @Override
    public void onActivityResumed(@NonNull Activity activity) {
        ActivityData activityData = activities.get(activity);
        if (activityData != null) {
            activityData.resumed = System.currentTimeMillis();
        }
    }

    @Override
    public void onActivityPaused(@NonNull Activity activity) {
        ActivityData activityData = activities.get(activity);
        if (activityData != null) {
            activityData.resumed = -1;
        }
    }

    @Override
    public void onActivityStopped(@NonNull Activity activity) {
        ActivityData activityData = activities.get(activity);
        if (activityData != null) {
            activityData.started = -1;
        }
    }

    @Override
    public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {

    }

    @Override
    public void onActivityDestroyed(@NonNull Activity activity) {
        activities.remove(activity);
    }

    private static class ActivityData {

        public final Activity activity;
        public long started;
        public long resumed;

        private ActivityData(Activity activity) {
            this.activity = activity;
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接