Android: ¿Cómo puedo obtener la actividad actual en primer plano (de un servicio)?

180

¿Existe una forma nativa de Android para obtener una referencia a la Actividad actualmente en ejecución desde un servicio?

Tengo un servicio ejecutándose en segundo plano, y me gustaría actualizar mi Actividad actual cuando ocurra un evento (en el servicio). ¿Hay una manera fácil de hacer eso (como la que sugerí anteriormente)?

Jorge
fuente
tal vez esto pueda darle una idea stackoverflow.com/a/28423385/185022
AZ_

Respuestas:

87

¿Existe una forma nativa de Android para obtener una referencia a la Actividad actualmente en ejecución desde un servicio?

No puede ser el propietario de la "Actividad actualmente en ejecución".

Tengo un servicio ejecutándose en segundo plano, y me gustaría actualizar mi Actividad actual cuando ocurra un evento (en el servicio). ¿Hay una manera fácil de hacer eso (como la que sugerí anteriormente)?

  1. Enviar una transmisión Intenta la actividad: aquí hay un proyecto de muestra que demuestra este patrón
  2. Haga que la actividad proporcione un PendingIntent(por ejemplo, vía createPendingResult()) que el servicio invoca
  3. Haga que la actividad registre una devolución de llamada u objeto de escucha con el servicio a través bindService(), y haga que el servicio llame a un método de evento en ese objeto de devolución de llamada / escucha
  4. Envíe una transmisión ordenada Intenta la actividad, con una prioridad baja BroadcastReceivercomo respaldo (para generar un mensajeNotification si la actividad no está en la pantalla): aquí hay una publicación de blog con más información sobre este patrón
CommonsWare
fuente
3
Gracias, pero como tengo muchas actividades y no quiero actualizarlas todas, estaba buscando una forma de Android para obtener la actividad de primer plano. Como dices, no debería poder hacer eso. Significa que tengo que sortear esto.
George
1
@George: Lo que quieras no te ayudaría, de todos modos. Como señala, tiene "un montón de actividades". Por lo tanto, esas son todas clases separadas. Por lo tanto, no hay nada que su servicio pueda hacer con ellos sin un bloque de instanceofcontroles masivo o refactorizando las actividades para compartir una superclase o interfaz común. Y si va a refactorizar las actividades, también puede hacerlo de una manera que se ajuste mejor al marco y cubra más escenarios, como que ninguna de sus actividades esté activa. # 4 es probablemente el menos trabajo y el más flexible.
CommonsWare
2
Gracias, pero tengo una mejor solución. Todas mis actividades amplían una clase personalizada de BaseActivity. He establecido un ContextRegister que registra la actividad como actual siempre que esté en primer plano, con 3 líneas en mi clase BaseActivity. Aún así, gracias por el apoyo.
George
3
@George: Cuidado con las pérdidas de memoria. Los miembros de datos estáticos mutables deben evitarse en Java siempre que sea posible.
CommonsWare
Encapsulé firmemente el objeto, aunque lo tendré en cuenta. Gracias.
George
139

Actualización : esto ya no funciona con las actividades de otras aplicaciones a partir de Android 5.0


Aquí hay una buena manera de hacerlo usando el administrador de actividades. Básicamente obtienes las RunningTasks del administrador de actividades. Siempre devolverá primero la tarea actualmente activa. Desde allí puede obtener la actividad superior.

Ejemplo aquí

Hay una manera fácil de obtener una lista de tareas en ejecución del servicio ActivityManager. Puede solicitar un número máximo de tareas que se ejecutan en el teléfono y, de manera predeterminada, la tarea activa actual se devuelve primero.

Una vez que tenga eso, puede obtener un objeto ComponentName solicitando topActivity de su lista.

Aquí hay un ejemplo.

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

Necesitará el siguiente permiso en su manifiesto:

<uses-permission android:name="android.permission.GET_TASKS"/>
Nelson Ramirez
fuente
3
Su respuesta es 100% correcta. Gracias 4. Es mejor que publique la misma respuesta aquí
Shahzad Imam
1
@ArtOfWarfare no se ha verificado, pero sí, la cantidad de fragmentos que tiene es irrelevante, ya que siempre están alojados en una sola actividad.
Nelson Ramirez
2
Si necesito la actividad actual con más frecuencia, tengo que sondear con mucha frecuencia, ¿verdad? ¿Hay alguna forma de obtener un evento de devolución de llamada cada vez que cambia la actividad en primer plano?
nizam.sp
43
Para que todos sepan, los documentos indican esto sobre el método getRunningTasks (). -------- Nota: este método solo está destinado a depurar y presentar interfaces de usuario de gestión de tareas. Esto nunca debe usarse para la lógica central en una aplicación, como decidir entre diferentes comportamientos en función de la información que se encuentra aquí. Dichos usos no son compatibles y es probable que se rompan en el futuro. Por ejemplo, si varias aplicaciones pueden ejecutarse activamente al mismo tiempo, las suposiciones hechas sobre el significado de los datos aquí para propósitos de control de flujo serán incorrectas. ------------
Ryan
30
semifake el 21 de As of LOLLIPOP, this method 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.
abril
116

Advertencia: violación de Google Play

Google ha amenazado con eliminar aplicaciones de Play Store si usan servicios de accesibilidad para fines de no accesibilidad. Sin embargo, se informa que esto se está reconsiderando .


Usar un AccessibilityService

Beneficios

  • Probado y funcionando en Android 2.2 (API 8) a través de Android 7.1 (API 25).
  • No requiere sondeo.
  • No requiere el GET_TASKS permiso.

Desventajas

  • Cada usuario debe habilitar el servicio en la configuración de accesibilidad de Android.
  • Esto no es 100% confiable. Ocasionalmente los eventos están fuera de servicio.
  • El servicio siempre se está ejecutando.
  • Cuando un usuario intenta habilitar AccessibilityService, no puede presionar el botón Aceptar si una aplicación ha colocado una superposición en la pantalla. Algunas aplicaciones que hacen esto son Velis Auto Brightness y Lux. Esto puede ser confuso porque el usuario puede no saber por qué no puede presionar el botón o cómo solucionarlo.
  • El AccessibilityServiceno sabrá la actividad actual hasta el primer cambio de actividad.

Ejemplo

Servicio

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

Combina esto en tu manifiesto:

<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>

Información de servicio

Pon esto en 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"/>

Habilitar el servicio

Cada usuario de la aplicación deberá habilitar explícitamente el AccessibilityServicepara que pueda ser utilizado. Ver esta respuesta de StackOverflow para cómo hacer esto.

Tenga en cuenta que el usuario no podrá presionar el botón Aceptar cuando intente habilitar el servicio de accesibilidad si una aplicación ha colocado una superposición en la pantalla, como Velis Auto Brightness o Lux.

Sam
fuente
1
@lovemint, lo que quise decir es: especifiqué un ejemplo settingsActivityde your.app.ServiceSettingsActivity, por lo que debe cambiarlo a su propia actividad de configuración para su servicio de accesibilidad. Creo que la actividad de configuración es opcional de todos modos, así que eliminé esa parte de mi respuesta para simplificarla.
Sam
1
Muchas gracias, solo la última pregunta. Si necesito tratar desde la API 8 a la actual, ¿debería hacer eso trabajar en el código en lugar de usar XML?
paolo2988
1
@lovemint, eso es correcto. Acabo de actualizar el código de ejemplo para hacer esto.
Sam
1
@ Sam, puedo confirmar que este servicio funciona perfectamente. Un comportamiento extraño, utilicé una intención en la actividad principal para habilitar el Servicio de Accesibilidad. Después de esta primera activación, el servicio se activa pero onAccessibilityEventno recibe ningún evento, pero si deshabilito y habilito nuevamente el Servicio de Accesibilidad, el servicio se reactiva y onAccessibilityEventcomienza a funcionar.
paolo2988
2
Gracias por tu código. Creé esta aplicación para ti :)
bóveda
20

Se puede hacer por:

  1. Implemente su propia clase de aplicación, regístrese en ActivityLifecycleCallbacks, de esta manera puede ver lo que está sucediendo con nuestra aplicación. En cada reanudación, la devolución de llamada asigna la actividad visible actual en la pantalla y, en pausa, elimina la asignación. Utiliza el método registerActivityLifecycleCallbacks()que se agregó en API 14.

    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. En su llamada de servicio getApplication()y transmítala al nombre de la clase de su aplicación (Aplicación en este caso). De lo que puede llamar app.getActiveActivity(), eso le dará una Actividad visible actual (o nula cuando no hay actividad visible). Puede obtener el nombre de la Actividad llamandoactiveActivity.getClass().getSimpleName()

Vit Veres
fuente
1
Recibo la excepción Nullpointer en activeActivity.getClass (). GetSimpleName (). ¿Pueden ayudarme
Principiante
Bueno, como puede ver en ActivityLifecycleCallbacks: en caso de que la actividad no sea visible, application.getActiveActivity () devuelve nulo. Eso significa que no hay actividad visible. Debe verificar esto en su servicio o en cualquier otro lugar donde lo use.
Vit Veres
ok..pero en caso de que la actividad se reanude, ¿esto no debería devolver nulo? Mi actividad se había iniciado y en su currículum vitae lo conseguí
principiante el
Difícil de adivinar, tratar de poner puntos de ruptura en onActivityResumed(), onActivityPaused()y para getActiveActivity()ver cómo se Callet, por lo que si alguna vez.
Vit Veres
@beginner hay que comprobar si activityy activeActivityson los mismos antes de asignar activeActivitycon nullel fin de errores Evita debido al fin de llamar entremezclado de los métodos del ciclo de vida de las diversas actividades.
Rahul Tiwari
16

No pude encontrar una solución con la que nuestro equipo estaría contento, así que desarrollamos la nuestra. Usamos ActivityLifecycleCallbackspara hacer un seguimiento de la actividad actual y luego exponerla a través de un servicio:

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

Luego configure su contenedor DI para devolver la instancia de MyApplicationfor ContextProvider, eg

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

(Tenga en cuenta que la implementación de getCurrent()se omite del código anterior. Es solo una variable estática que se establece desde el constructor de la aplicación)

Muxa
fuente
44
hay que comprobar si activityy currentActivityson los mismos antes de asignar currentActivitycon nullel fin de errores Evita debido al fin de llamar entremezclado de los métodos del ciclo de vida de las diversas actividades.
Rahul Tiwari el
14

Utilizar ActivityManager

Si solo desea conocer la aplicación que contiene la actividad actual, puede hacerlo utilizando ActivityManager. La técnica que puede usar depende de la versión de Android:

Beneficios

  • Debería funcionar en todas las versiones de Android hasta la fecha.

Desventajas

  • No funciona en Android 5.1+ (solo devuelve tu propia aplicación)
  • La documentación para estas API dice que solo están destinadas a la depuración y administración de interfaces de usuario.
  • Si desea actualizaciones en tiempo real, debe usar el sondeo.
  • Se basa en una API oculta: ActivityManager.RunningAppProcessInfo.processState
  • Esta implementación no recoge la actividad de cambio de aplicación.

Ejemplo (basado en el código de KNaito )

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

}

Manifiesto

Agregue el GET_TASKSpermiso a AndroidManifest.xml:

<!--suppress DeprecatedClassUsageInspection -->
<uses-permission android:name="android.permission.GET_TASKS" />
Sam
fuente
10
Esto ya no funciona en Android M. getRunningAppProcesses () ahora solo devuelve el paquete de su aplicación
Lior Iluz
1
Tampoco funciona para API Nivel 22 (Android 5.1). Probado en Build LPB23
sumitb.mdi
9

Estoy usando esto para mis pruebas. Sin embargo, es API> 19, y solo para actividades de su aplicación.

@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");
}
espinchi
fuente
Reemplace ArrayMap por Map y también funcionará en 4.3. No probado versiones anteriores de Android.
Oliver Jonas el
2

Use este código para API 21 o superior. Esto funciona y ofrece mejores resultados en comparación con las otras respuestas, detecta perfectamente el proceso en primer plano.

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();
        }
    }
Manish Godhani
fuente
¿Puede explicar cómo los resultados de este método son mejores que las otras respuestas?
Sam
¿Funciona esto también con el menú de notificaciones? Estoy ante un problema en el que recibo una llamada o un mensaje. UsageStatsManager comienza a darme el nombre del paquete de systemUi en lugar de mi aplicación.
kukroid
@kukroid, publique una pregunta por separado e incluya su código fuente, información del dispositivo y qué aplicaciones de mensajería y teléfono está utilizando.
Sam
0

Aquí está mi respuesta que funciona bien ...

Debería poder obtener la Actividad actual de esta manera ... Si estructura su aplicación con algunas Actividades con muchos fragmentos y desea realizar un seguimiento de cuál es su Actividad actual, requeriría mucho trabajo. Mi senario fue que tengo una actividad con múltiples fragmentos. Por lo tanto, puedo realizar un seguimiento de la actividad actual a través del objeto de aplicación, que puede almacenar todo el estado actual de las variables globales.

Aquí hay un camino. Cuando inicia su Actividad, almacena esa Actividad por Application.setCurrentActivity (getIntent ()); Esta aplicación lo almacenará. En su clase de servicio, simplemente puede hacer como Intent currentIntent = Application.getCurrentActivity (); getApplication (). startActivity (currentIntent);

John3328
fuente
2
Esto supone que el servicio se ejecuta en el mismo proceso que la Actividad, ¿verdad?
helleye
0

No sé si es una respuesta estúpida, pero resolví este problema almacenando una marca en las preferencias compartidas cada vez que ingresé en Crear () de cualquier actividad, luego utilicé el valor de las preferencias compartidas para descubrir cuál es la actividad en primer plano.

Vlad Vladescu
fuente
1
Estamos buscando una forma nativa de hacerlo
IgniteCoders
-3

Recientemente me enteré de esto. Con apis como:

  • minSdkVersion 19
  • targetSdkVersion 26

    ActivityManager.getCurrentActivity (contexto)

Espero que esto sea de alguna utilidad.

usuario3709410
fuente
1
Tenía muchas esperanzas. Quizás cuando esto fue respondido existía, pero a partir de ahora no hay ninguna getCurrentActivity()función en la ActivityManagerclase ( developer.android.com/reference/android/app/ActivityManager ). Es curioso que esta función no existe: ActivityManager.isUserAMonkey().
ByteSlinger
1
Esto detecta si está utilizando un agente de prueba de mono para probar su actividad. Aunque es un nombre divertido, podría ser bastante útil para probar (y hacer reír a otras personas, obviamente) developer.android.com/studio/test/monkeyrunner
Ken Corey