Comment empêcher plusieurs instances d'une activité lorsqu'elle est lancée avec des intentions différentes

121

J'ai rencontré un bug dans mon application lors de son lancement à l'aide du bouton «Ouvrir» de l'application Google Play Store (anciennement appelée Android Market). Il semble que son lancement à partir du Play Store utilise une méthode différente Intentde celle du menu d'icônes du téléphone. Cela conduit au lancement de plusieurs copies de la même activité, qui sont en conflit les unes avec les autres.

Par exemple, si mon application se compose de l'ABC des activités, ce problème peut conduire à une pile d'ABCA.

J'ai essayé d'utiliser android:launchMode="singleTask"toutes les activités pour résoudre ce problème, mais cela a pour effet secondaire indésirable d'effacer la pile d'activités à la racine, chaque fois que je clique sur le bouton HOME.

Le comportement attendu est: ABC -> HOME -> Et quand l'application est restaurée, j'ai besoin de: ABC -> HOME -> ABC

Existe-t-il un bon moyen d'empêcher le lancement de plusieurs activités du même type, sans réinitialiser l'activité racine lors de l'utilisation du bouton ACCUEIL?

bsberkeley
la source

Réponses:

187

Ajoutez ceci à onCreate et vous devriez être prêt à partir:

// Possible work around for market launches. See https://issuetracker.google.com/issues/36907463
// for more details. Essentially, the market launches the main activity on top of other activities.
// we never want this to happen. Instead, we check if we are the root and if not, we finish.
if (!isTaskRoot()) {
    final Intent intent = getIntent();
    if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(intent.getAction())) {
        Log.w(LOG_TAG, "Main Activity is not the root.  Finishing Main Activity instead of launching.");
        finish();
        return;       
    }
}
Duane Homick
la source
25
J'essaie de résoudre ce bogue depuis des années, et c'est la solution qui a fonctionné, alors merci beaucoup! Je dois également noter que ce n'est pas seulement un problème au sein de l'Android Market, mais également que le chargement d'une application en la téléchargeant sur un serveur ou en l'envoyant par courrier électronique sur votre téléphone pose ce problème. Toutes ces choses installent l'application à l'aide du programme d'installation de package, où je pense que le bogue réside. De plus, juste au cas où cela ne serait pas clair, il vous suffit d'ajouter ce code à la méthode onCreate de votre activité racine.
ubzack
2
Je trouve très étrange que cela se produise dans une application signée déployée sur l'appareil, mais pas dans une version de débogage déployée à partir d'Eclipse. Rend le débogage assez difficile!
Matt Connolly
6
Cela ne se produit avec une version de débogage déployée depuis Eclipse aussi longtemps que vous le lancez aussi via Eclipse (ou IntelliJ ou autre IDE). Cela n'a rien à voir avec la façon dont l'application est installée sur l'appareil. Le problème est dû à la façon dont l'application est lancée .
David Wasser
2
Quelqu'un sait-il si ce code garantira que l'instance existante de l'application sera mise au premier plan? Ou appelle-t-il simplement finish (); et laisser l'utilisateur sans indication visuelle que quelque chose s'est passé?
Carlos P
5
@CarlosP si l'activité en cours de création n'est pas l'activité racine de la tâche, il doit y avoir (par définition) au moins une autre activité en dessous. Si cette activité appelle, finish()l'utilisateur verra l'activité qui se trouvait en dessous. Pour cette raison, vous pouvez supposer en toute sécurité que l'instance existante de l'application sera mise au premier plan. Si ce n'était pas le cas, vous auriez plusieurs instances de l'application dans des tâches distinctes et l'activité en cours de création serait la racine de sa tâche.
David Wasser
27

Je vais juste expliquer pourquoi cela échoue et comment reproduire ce bogue par programme afin que vous puissiez l'incorporer dans votre suite de tests:

  1. Lorsque vous lancez une application via Eclipse ou Market App, elle se lance avec les indicateurs d'intention: FLAG_ACTIVITY_NEW_TASK.

  2. Lors du lancement via le lanceur (home), il utilise les indicateurs: FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_BROUGHT_TO_FRONT | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED, et utilise l'action " MAIN " et la catégorie " LAUNCHER ".

Si vous souhaitez reproduire ceci dans un cas de test, procédez comme suit:

adb shell am start -f 0x10000000 -n com.testfairy.tests.regression.taskroot/.MainActivity 

Ensuite, faites tout ce qui est nécessaire pour accéder à l'autre activité. Pour mes besoins, je viens de placer un bouton qui démarre une autre activité. Revenez ensuite au lanceur (accueil) avec:

adb shell am start -W -c android.intent.category.HOME -a android.intent.action.MAIN

Et simulez son lancement via le lanceur avec ceci:

adb shell am start -a "android.intent.action.MAIN" -c "android.intent.category.LAUNCHER" -f 0x10600000 -n com.testfairy.tests.regression.taskroot/.MainActivity

Si vous n'avez pas incorporé la solution de contournement isTaskRoot (), cela reproduira le problème. Nous utilisons cela dans nos tests automatiques pour nous assurer que ce bogue ne se reproduira plus.

J'espère que cela t'aides!

gilm
la source
8

Avez-vous essayé le mode de lancement singleTop ?

Voici une partie de la description de http://developer.android.com/guide/topics/manifest/activity-element.html :

... une nouvelle instance d'une activité "singleTop" peut également être créée pour gérer une nouvelle intention. Cependant, si la tâche cible a déjà une instance existante de l'activité en haut de sa pile, cette instance recevra le nouvel intent (dans un appel onNewIntent ()); une nouvelle instance n'est pas créée. Dans d'autres circonstances - par exemple, si une instance existante de l'activité "singleTop" se trouve dans la tâche cible, mais pas en haut de la pile, ou si elle se trouve en haut d'une pile, mais pas dans la tâche cible - a une nouvelle instance serait créée et poussée sur la pile.

Eric Levine
la source
2
J'y ai pensé, mais que faire si l'activité n'est pas au sommet de la pile? Par exemple, il semble que singleTop empêchera AA, mais pas ABA.
bsberkeley
Pouvez-vous réaliser ce que vous voulez en utilisant singleTop et les méthodes de finition dans Activity?
Eric Levine
Je ne sais pas si cela accomplira tout à fait ce que je veux. Exemple: si je suis sur l'activité C après avoir fait apparaître A et B, alors une nouvelle activité A est lancée et j'aurai quelque chose comme CA, n'est-ce pas?
bsberkeley
Il est difficile de répondre à cela sans mieux comprendre ce que font ces activités. Pouvez-vous fournir plus de détails sur votre candidature et les activités? Je me demande s'il y a un décalage entre ce que fait le bouton d'accueil et comment vous voulez qu'il agisse. Le bouton d'accueil ne permet pas de quitter une activité, il "l'arrière-plan" pour que l'utilisateur puisse passer à autre chose. Le bouton de retour est ce qui sort / termine et l'activité. Briser ce paradigme pourrait dérouter / frustrer les utilisateurs.
Eric Levine
J'ai ajouté une autre réponse à ce fil afin que vous puissiez voir une copie du manifeste.
bsberkeley
4

C'est peut-être ce problème ? Ou une autre forme du même bug?

DuneCat
la source
Voir également code.google.com/p/android/issues/detail?id=26658 , qui démontre que cela est causé par des choses autres qu'Eclipse.
Kristopher Johnson
1
Je devrais donc copier-coller une description de problème qui pourrait devenir obsolète? Quelles parties? Les parties essentielles doivent-elles être conservées si le lien change, et est-ce ma responsabilité que la réponse soit tenue à jour? On devrait penser que le lien ne devient invalide que si le problème est résolu. Ce n'est pas un lien vers un blog, après tout.
DuneCat
2

Je pense que la réponse acceptée ( Duane Homick ) a des cas non traités :

Vous avez différents extras (et des doublons d'application en conséquence):

  • lorsque vous lancez l'application depuis Market ou par l'icône de l'écran d'accueil (qui est automatiquement placée par Market)
  • lorsque vous lancez l'application par le lanceur ou l'icône d'écran d'accueil créée manuellement

Voici une solution (SDK_INT> = 11 pour les notifications) que je crois gérer ces cas et les notifications de la barre d'état également.

Manifeste :

    <activity
        android:name="com.acme.activity.LauncherActivity"
        android:noHistory="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </activity>
    <service android:name="com.acme.service.LauncherIntentService" />

Activité du lanceur :

public static Integer lastLaunchTag = null;
@Override
public void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mInflater = LayoutInflater.from(this);
    View mainView = null;
    mainView = mInflater.inflate(R.layout.act_launcher, null); // empty layout
    setContentView(mainView);

    if (getIntent() == null || getIntent().getExtras() == null || !getIntent().getExtras().containsKey(Consts.EXTRA_ACTIVITY_LAUNCH_FIX)) {
        Intent serviceIntent = new Intent(this, LauncherIntentService.class);
        if (getIntent() != null && getIntent().getExtras() != null) {
            serviceIntent.putExtras(getIntent().getExtras());
        }
        lastLaunchTag = (int) (Math.random()*100000);
        serviceIntent.putExtra(Consts.EXTRA_ACTIVITY_LAUNCH_TAG, Integer.valueOf(lastLaunchTag));
        startService(serviceIntent);

        finish();
        return;
    }

    Intent intent = new Intent(this, SigninActivity.class);
    if (getIntent() != null && getIntent().getExtras() != null) {
        intent.putExtras(getIntent().getExtras());
    }
    startActivity(intent);
}

Service :

@Override
protected void onHandleIntent(final Intent intent) {
    Bundle extras = intent.getExtras();
    Integer lastLaunchTag = extras.getInt(Consts.EXTRA_ACTIVITY_LAUNCH_TAG);

    try {
        Long timeStart = new Date().getTime(); 
        while (new Date().getTime() - timeStart < 100) {
            Thread.currentThread().sleep(25);
            if (!lastLaunchTag.equals(LauncherActivity.lastLaunchTag)) {
                break;
            }
        }
        Thread.currentThread().sleep(25);
        launch(intent);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

private void launch(Intent intent) {
    Intent launchIintent = new Intent(LauncherIntentService.this, LauncherActivity.class);
    launchIintent.addCategory(Intent.CATEGORY_LAUNCHER);
    launchIintent.setAction(Intent.ACTION_MAIN); 
    launchIintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    launchIintent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 
    if (intent != null && intent.getExtras() != null) {
        launchIintent.putExtras(intent.getExtras());
    }
    launchIintent.putExtra(Consts.EXTRA_ACTIVITY_LAUNCH_FIX, true);
    startActivity(launchIintent);
}

Notification :

ComponentName actCN = new ComponentName(context.getPackageName(), LauncherActivity.class.getName()); 
Intent contentIntent = new Intent(context, LauncherActivity.class);
contentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);    
if (Build.VERSION.SDK_INT >= 11) { 
    contentIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); // if you need to recreate activity stack
}
contentIntent.addCategory(Intent.CATEGORY_LAUNCHER);
contentIntent.setAction(Intent.ACTION_MAIN);
contentIntent.putExtra(Consts.EXTRA_CUSTOM_DATA, true);
StanislavKo
la source
2

Je me rends compte que la question n'a rien à voir avec Xamarin Android mais je voulais publier quelque chose car je ne l'ai vu nulle part ailleurs.

Pour résoudre ce problème dans Xamarin Android, j'ai utilisé le code de @DuaneHomick et ajouté dans MainActivity.OnCreate(). La différence avec Xamarin est qu'il faut aller après Xamarin.Forms.Forms.Init(this, bundle);et LoadApplication(new App());. Donc, mon OnCreate()ressemblerait à:

protected override void OnCreate(Bundle bundle) {
    base.OnCreate(bundle);

    Xamarin.Forms.Forms.Init(this, bundle);
    LoadApplication(new App());

    if(!IsTaskRoot) {
        Intent intent = Intent;
        string action = intent.Action;
        if(intent.HasCategory(Intent.CategoryLauncher) && action != null && action.Equals(Intent.ActionMain, System.StringComparison.OrdinalIgnoreCase)) {
            System.Console.WriteLine("\nIn APP.Droid.MainActivity.OnCreate() - Finishing Activity and returning since a second MainActivity has been created.\n");
            Finish();
            return; //Not necessary if there is no code below
        }
    }
}

* Edit: Depuis Android 6.0, la solution ci-dessus n'est pas suffisante pour certaines situations. Je me suis maintenant également mis LaunchModeà SingleTask, ce qui semble avoir à nouveau fait fonctionner correctement les choses. Je ne sais malheureusement pas quels effets cela pourrait avoir sur d'autres choses.

hvaughan3
la source
0

J'ai eu le même problème et je l'ai résolu en utilisant la solution suivante.

Dans votre activité principale, ajoutez ce code en haut de la onCreateméthode:

ActivityManager manager = (ActivityManager) this.getSystemService( ACTIVITY_SERVICE );
List<RunningTaskInfo> tasks =  manager.getRunningTasks(Integer.MAX_VALUE);

for (RunningTaskInfo taskInfo : tasks) {
    if(taskInfo.baseActivity.getClassName().equals(<your package name>.<your class name>) && (taskInfo.numActivities > 1)){
        finish();
    }
}

n'oubliez pas d'ajouter cette autorisation dans votre manifeste.

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

j'espère que cela vous aide.

gugarush
la source
0

J'ai aussi eu ce problème

  1. N'appelez pas finish (); dans l'activité domestique, il s'exécuterait sans fin - l'activité domestique est appelée par ActivityManager lorsqu'elle est terminée.
  2. Habituellement, lorsque la configuration change (c.-à-d. Faire pivoter l'écran, changer de langue, le service de téléphonie change, c.-à-d. Mcc mnc, etc.), l'activité recrée - et si l'activité domestique est en cours d'exécution, elle appelle à nouveau A. pour ce besoin d'ajouter au manifeste android:configChanges="mcc|mnc"- si vous avez une connexion au cellulaire, voir http://developer.android.com/guide/topics/manifest/activity-element.html#config pour savoir quelle configuration il y a lors du démarrage du système ou push open ou autre.
user1249350
la source
0

Essayez cette solution:
créez une Applicationclasse et définissez-y:

public static boolean IS_APP_RUNNING = false;

Ensuite, dans votre première activité (Launcher) onCreateavant d' setContentView(...)ajouter ceci:

if (Controller.IS_APP_RUNNING == false)
{
  Controller.IS_APP_RUNNING = true;
  setContentView(...)
  //Your onCreate code...
}
else
  finish();

PS Controllerest ma Applicationclasse.

Volodymyr Kulyk
la source
Vous devriez utiliser un booléen primitif, ce qui rend inutile la vérification de null.
WonderCsabo
Cela ne fonctionnera pas toujours. Vous ne pourrez jamais lancer votre application, quitter votre application, puis relancer rapidement votre application. Android ne tue pas nécessairement le processus du système d'exploitation d'hébergement dès qu'il n'y a aucune activité active. Dans ce cas, lorsque vous redémarrez l'application, la variable IS_APP_RUNNINGsera trueet votre application se fermera immédiatement. Ce n'est pas quelque chose que l'utilisateur trouvera probablement amusant.
David Wasser le
-2

essayez d'utiliser le mode de lancement SingleInstance avec l'affinité définie pour autoriser la préparation de la tâche Cela créera toujours l'activité dans une nouvelle tâche mais permettra également son reparenting. Vérifier les: attribut d'affinité

Shaireen
la source
2
Cela ne fonctionnera probablement pas car, selon la documentation, "la re-parentalité est limitée aux modes" standard "et" singleTop "." parce que les "activités avec les modes de lancement" singleTask "ou" singleInstance "ne peuvent être qu'à la racine d'une tâche"
bsberkeley
-2

J'ai trouvé un moyen d'éviter de commencer les mêmes activités, cela fonctionne très bien pour moi

if ( !this.getClass().getSimpleName().equals("YourActivityClassName")) {
    start your activity
}
Odhik Susanto
la source