Android: comment puis-je obtenir l'activité actuelle au premier plan (à partir d'un service)?

180

Existe-t-il un moyen Android natif pour obtenir une référence à l'activité en cours d'exécution à partir d'un service?

J'ai un service en cours d'exécution en arrière-plan et je souhaite mettre à jour mon activité actuelle lorsqu'un événement se produit (dans le service). Existe-t-il un moyen simple de le faire (comme celui que j'ai suggéré ci-dessus)?

George
la source
peut-être que cela peut vous donner une idée stackoverflow.com/a/28423385/185022
AZ_

Réponses:

87

Existe-t-il un moyen Android natif pour obtenir une référence à l'activité en cours d'exécution à partir d'un service?

Vous n'êtes peut-être pas propriétaire de «l'activité en cours d'exécution».

J'ai un service en cours d'exécution en arrière-plan et je souhaite mettre à jour mon activité actuelle lorsqu'un événement se produit (dans le service). Existe-t-il un moyen simple de le faire (comme celui que j'ai suggéré ci-dessus)?

  1. Envoyez une diffusion Intentà l'activité - voici un exemple de projet illustrant ce modèle
  2. Demandez à l'activité de fournir un PendingIntent(par exemple, via createPendingResult()) que le service appelle
  3. Demandez à l'activité d'enregistrer un objet de rappel ou d'écoute avec le service via bindService(), et que le service appelle une méthode d'événement sur cet objet de rappel / d'écoute
  4. Envoyez une diffusion ordonnée Intentà l'activité, avec une priorité faible BroadcastReceivercomme sauvegarde (pour augmenter une Notificationsi l'activité n'est pas à l'écran) - voici un article de blog avec plus d'informations sur ce modèle
CommonsWare
la source
3
Merci, mais comme j'ai une série d'activités et que je ne veux pas toutes les mettre à jour, je cherchais un moyen Android pour obtenir le premier planActivité. Comme vous le dites, je ne devrais pas être capable de faire cela. Cela signifie que je dois contourner ce problème.
George
1
@George: Ce que vous voulez ne vous aiderait pas, de toute façon. Comme vous le faites remarquer, vous avez «une série d'activités». Par conséquent, ce sont toutes des classes distinctes. Par conséquent, il n'y a rien que votre service puisse faire avec eux sans un si-bloc massif de instanceofvérifications ou sans refactoriser les activités pour partager une superclasse ou une interface commune. Et si vous comptez refactoriser les activités, vous pouvez tout aussi bien le faire d'une manière qui correspond mieux au cadre et couvre plus de scénarios, comme aucune de vos activités n'est active. Le n ° 4 est probablement le moins de travail et le plus flexible.
CommonsWare
2
Merci, mais j'ai une meilleure solution. Toutes mes activités étendent une classe BaseActivity personnalisée. J'ai défini un ContextRegister qui enregistre l'activité comme actuelle chaque fois qu'elle est au premier plan, avec literraly 3 lignes dans ma classe BaseActivity. Pourtant, merci pour le soutien.
George
3
@George: Attention aux fuites de mémoire. Les membres de données statiques mutables doivent être évités dans la mesure du possible en Java.
CommonsWare
J'ai étroitement encapsulé l'objet, même si je garderai cela à l'esprit. Merci.
George
139

Mise à jour : cela ne fonctionne plus avec les activités des autres applications à partir d'Android 5.0


Voici une bonne façon de le faire en utilisant le gestionnaire d'activités. Vous obtenez essentiellement les runningTasks du gestionnaire d'activités. Il retournera toujours la tâche actuellement active en premier. De là, vous pouvez obtenir le topActivity.

Exemple ici

Il existe un moyen simple d'obtenir une liste des tâches en cours d'exécution à partir du service ActivityManager. Vous pouvez demander un nombre maximum de tâches en cours d'exécution sur le téléphone et, par défaut, la tâche actuellement active est renvoyée en premier.

Une fois que vous avez cela, vous pouvez obtenir un objet ComponentName en demandant le topActivity de votre liste.

Voici un exemple.

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

Vous aurez besoin de l'autorisation suivante sur votre manifeste:

<uses-permission android:name="android.permission.GET_TASKS"/>
Nelson Ramirez
la source
3
Votre réponse est 100% correcte.Merci 4 it.It mieux que vous postez la même réponse ici
Shahzad Imam
1
@ArtOfWarfare n'a pas vérifié mais oui, le nombre de fragments que vous avez n'est pas pertinent car ils sont toujours hébergés par une seule activité.
Nelson Ramirez
2
Si j'ai besoin de l'activité en cours plus fréquemment, je dois interroger très fréquemment, n'est-ce pas? Existe-t-il un moyen d'obtenir un événement de rappel chaque fois que l'activité de premier plan change?
nizam.sp
43
Juste pour que tout le monde le sache, la documentation le déclare à propos de la méthode getRunningTasks (). -------- Remarque: cette méthode est uniquement destinée au débogage et à la présentation des interfaces utilisateur de gestion des tâches. Cela ne doit jamais être utilisé pour la logique de base dans une application, comme le choix entre différents comportements en fonction des informations trouvées ici. De telles utilisations ne sont pas prises en charge et seront probablement interrompues à l'avenir. Par exemple, si plusieurs applications peuvent s'exécuter activement en même temps, les hypothèses faites sur la signification des données ici à des fins de flux de contrôle seront incorrectes. ------------
Ryan
30
semifake sur api 21As 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.
sherpya
116

Avertissement: violation de Google Play

Google a menacé de supprimer des applications du Play Store si elles utilisent des services d'accessibilité à des fins non d'accessibilité. Cependant, cela serait en cours de réexamen .


Utilisez un AccessibilityService

Avantages

  • Testé et fonctionnant sous Android 2.2 (API 8) via Android 7.1 (API 25).
  • Ne nécessite pas de sondage.
  • Ne nécessite pas la GET_TASKSpermission.

Désavantages

  • Chaque utilisateur doit activer le service dans les paramètres d'accessibilité d'Android.
  • Ce n'est pas fiable à 100%. Parfois, les événements sont dans le désordre.
  • Le service est toujours en cours d'exécution.
  • Lorsqu'un utilisateur tente d'activer le AccessibilityService, il ne peut pas appuyer sur le bouton OK si une application a placé une superposition à l'écran. Certaines applications qui font cela sont Velis Auto Brightness et Lux. Cela peut être déroutant car l'utilisateur peut ne pas savoir pourquoi il ne peut pas appuyer sur le bouton ou comment le contourner.
  • Ils AccessibilityServicene connaîtront pas l'activité en cours avant le premier changement d'activité.

Exemple

Un service

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

Fusionnez ceci dans votre manifeste:

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

Info service

Mettez ceci dans 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"/>

Activation du service

Chaque utilisateur de l'application devra activer explicitement le AccessibilityServicepour pouvoir l'utiliser. Consultez cette réponse StackOverflow pour savoir comment procéder.

Notez que l'utilisateur ne pourra pas appuyer sur le bouton OK lorsqu'il essaie d'activer le service d'accessibilité si une application a placé une superposition à l'écran, telle que Velis Auto Brightness ou Lux.

Sam
la source
1
@lovemint, ce que je voulais dire, c'est: j'ai spécifié un exemple settingsActivityde your.app.ServiceSettingsActivity, vous devriez donc le changer pour votre propre activité de paramètres pour votre service d'accessibilité. Je pense que l'activité des paramètres est facultative de toute façon, j'ai donc supprimé cette partie de ma réponse pour la rendre plus simple.
Sam le
1
Merci beaucoup, seule dernière question. Si je dois traiter de l'API 8 à l'actuel, je dois faire cela dans le code à la place en utilisant XML?
paolo2988
1
@lovemint, c'est vrai. Je viens de mettre à jour l'exemple de code pour ce faire.
Sam le
1
@Sam je peux confirmer que ce service fonctionne parfaitement. Un comportement étrange, j'ai utilisé une intention dans l'activité principale pour activer le service d'accessibilité. Après cette première activation, le service est activé mais onAccessibilityEventne reçoit aucun événement, mais si je désactive et réactive le service d'accessibilité, le service est réactivé et onAccessibilityEventcommence à fonctionner.
paolo2988 du
2
Merci pour votre code. J'ai créé cette application pour vous :)
vault
20

Cela peut être fait par:

  1. Implémentez votre propre classe d'application, inscrivez-vous à ActivityLifecycleCallbacks - de cette façon, vous pouvez voir ce qui se passe avec notre application. À chaque reprise, le rappel attribue l'activité visible actuelle à l'écran et en pause, il supprime l'affectation. Il utilise une méthode registerActivityLifecycleCallbacks()qui a été ajoutée dans l'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. Dans votre appel de service getApplication()et cast dans le nom de votre classe d'application (App dans ce cas). Que vous pouvez appeler app.getActiveActivity()- cela vous donnera une activité visible actuelle (ou nulle lorsqu'aucune activité n'est visible). Vous pouvez obtenir le nom de l'activité en appelantactiveActivity.getClass().getSimpleName()

Vit Veres
la source
1
Je reçois une exception Nullpointer à activeActivity.getClass (). GetSimpleName (). Pouvez-vous s'il vous plaît aider
débutant le
Eh bien, comme vous pouvez le voir dans ActivityLifecycleCallbacks - au cas où l'activité ne serait pas visible, application.getActiveActivity () renvoie null. Cela signifie qu'aucune activité n'est visible. Vous devez vérifier cela dans votre service ou partout où vous l'utilisez.
Vit Veres
ok..mais au cas où l'activité reprend, cela ne devrait pas renvoyer nul ?? Mon activité était lancée et dans son CV je l'ai eu
débutant
Difficile à deviner, essayez de mettre des points d'arrêt à onActivityResumed(), onActivityPaused()et getActiveActivity()de voir comment est-il callet, le cas échéant.
Vit Veres
@beginner il faut vérifier si activityet activeActivitysont les mêmes avant d'assigner activeActivityavecnull afin d'éviter les erreurs dues à l'ordre d'appel entremêlé des méthodes de cycle de vie de diverses activités.
Rahul Tiwari
16

Je n'ai pas trouvé de solution qui plairait à notre équipe, alors nous avons lancé la nôtre. Nous utilisons ActivityLifecycleCallbackspour garder une trace de l'activité actuelle, puis l'exposer via un service:

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

Configurez ensuite votre conteneur DI pour renvoyer l'instance de MyApplicationfor ContextProvider, par exemple

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

(Notez que l'implémentation de getCurrent()est omise du code ci-dessus. Il s'agit simplement d'une variable statique définie à partir du constructeur de l'application)

Muxa
la source
4
il faut vérifier si activityet currentActivitysont identiques avant d'affecter currentActivityavec nullafin d'éviter les erreurs dues à l'ordre d'appel entremêlé des méthodes du cycle de vie de diverses activités.
Rahul Tiwari
14

Utilisation ActivityManager

Si vous souhaitez uniquement connaître l' application contenant l'activité en cours, vous pouvez le faire en utilisant ActivityManager. La technique que vous pouvez utiliser dépend de la version d'Android:

Avantages

  • Devrait fonctionner dans toutes les versions d'Android à ce jour.

Désavantages

  • Ne fonctionne pas sous Android 5.1+ (il ne renvoie que votre propre application)
  • La documentation de ces API indique qu'elles sont uniquement destinées au débogage et à la gestion des interfaces utilisateur.
  • Si vous souhaitez des mises à jour en temps réel, vous devez utiliser l'interrogation.
  • S'appuie sur une API cachée: ActivityManager.RunningAppProcessInfo.processState
  • Cette implémentation ne détecte pas l'activité du sélecteur d'application.

Exemple (basé sur le code 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);
    }

}

Manifeste

Ajoutez l' GET_TASKSautorisation à AndroidManifest.xml:

<!--suppress DeprecatedClassUsageInspection -->
<uses-permission android:name="android.permission.GET_TASKS" />
Sam
la source
10
cela ne fonctionne plus sur Android M. getRunningAppProcesses () ne renvoie désormais que le package de votre application
Lior Iluz
1
Ne fonctionne pas non plus pour le niveau d'API 22 (Android 5.1). Testé sur Build LPB23
sumitb.mdi le
9

J'utilise ceci pour mes tests. C'est API> 19, et uniquement pour les activités de votre application.

@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
la source
Remplacez ArrayMap par Map et cela fonctionnera également sur 4.3. Non testé les versions antérieures d'Android.
Oliver Jonas
2

Utilisez ce code pour l'API 21 ou supérieur. Cela fonctionne et donne un meilleur résultat par rapport aux autres réponses, il détecte parfaitement le processus de premier plan.

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
la source
Pouvez-vous expliquer en quoi les résultats de cette méthode sont meilleurs que les autres réponses?
Sam
Cela fonctionne-t-il également avec le menu de notification. Je suis confronté à un problème dans lequel si je reçois un appel ou un message. UsageStatsManager commence à me donner le nom de package de systemUi au lieu de mon application.
kukroid
@kukroid, postez une question distincte et incluez votre code source, les informations sur l'appareil et les applications de messagerie et de téléphone que vous utilisez.
Sam
0

Voici ma réponse qui fonctionne très bien ...

Vous devriez pouvoir obtenir l'activité actuelle de cette manière ... Si vous structurez votre application avec quelques activités avec de nombreux fragments et que vous souhaitez garder une trace de votre activité actuelle, cela prendrait beaucoup de travail. Mon senario était que j'avais une activité avec plusieurs fragments. Ainsi, je peux garder une trace de l'activité actuelle via l'objet d'application, qui peut stocker tout l'état actuel des variables globales.

Voici un moyen. Lorsque vous démarrez votre activité, vous stockez cette activité par Application.setCurrentActivity (getIntent ()); Cette application le stockera. Sur votre classe de service, vous pouvez simplement faire comme Intent currentIntent = Application.getCurrentActivity (); getApplication (). startActivity (currentIntent);

John3328
la source
2
Cela suppose que le service s'exécute dans le même processus que l'activité, non?
helleye
0

Je ne sais pas si c'est une réponse stupide, mais j'ai résolu ce problème en stockant un drapeau dans les préférences partagées chaque fois que je suis entré dans onCreate () d'une activité, puis j'ai utilisé la valeur des préférences shered pour savoir en quoi consiste l'activité de premier plan.

Vlad Vladescu
la source
1
Nous recherchons une façon native de le faire
IgniteCoders
-3

Récemment découvert à ce sujet. Avec des API comme:

  • minSdkVersion 19
  • targetSdkVersion 26

    ActivityManager.getCurrentActivity (contexte)

J'espère que c'est utile.

user3709410
la source
1
J'avais mes espoirs. Peut-être que lorsque cela a été répondu, cela existait, mais pour le moment, il n'y a pas de getCurrentActivity()fonction dans la ActivityManagerclasse ( developer.android.com/reference/android/app/ActivityManager ). Drôle que cette fonction existe: ActivityManager.isUserAMonkey().
ByteSlinger
1
Cela détecte si vous utilisez un agent de test de singe pour tester votre activité. Bien que ce nom soit drôle, il pourrait être très utile pour tester (et faire rire les autres, bien entendu) developer.android.com/studio/test/monkeyrunner
Ken Corey