Come impedire più istanze di un'attività quando viene avviata con Intenti diversi


121

Ho riscontrato un bug nella mia applicazione quando viene avviata utilizzando il pulsante "Apri" sull'app Google Play Store (precedentemente chiamata Android Market). Sembra che avviarlo dal Play Store utilizzi un metodo diverso Intentrispetto all'avvio dal menu delle icone dell'applicazione del telefono. Ciò sta portando all'avvio di più copie della stessa attività, che sono in conflitto tra loro.

Ad esempio, se la mia app è costituita dalle attività ABC, questo problema può portare a una pila di ABCA.

Ho provato a utilizzare android:launchMode="singleTask"su tutte le attività per risolvere questo problema, ma ha l'effetto collaterale indesiderato di cancellare lo stack delle attività per eseguire il root, ogni volta che premo il pulsante HOME.

Il comportamento previsto è: ABC -> HOME -> E quando l'app viene ripristinata, ho bisogno di: ABC -> HOME -> ABC

C'è un buon modo per impedire l'avvio di più attività dello stesso tipo, senza ripristinare l'attività di root quando si utilizza il pulsante HOME?


Risposte:


187

Aggiungi questo a onCreate e dovresti essere pronto per iniziare:

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

25
Ho cercato di risolvere questo bug per anni e questa è stata la soluzione che ha funzionato, quindi grazie mille! Devo anche notare che questo non è solo un problema all'interno di Android Market, ma anche il sideload di un'app caricandola su un server o inviandola tramite e-mail al tuo telefono, causa questo problema. Tutte queste cose installano l'app utilizzando il Package Installer, dove credo risieda il bug. Inoltre, nel caso in cui non sia chiaro, devi solo aggiungere questo codice al metodo onCreate di qual è la tua attività di root.
ubzack

2
Trovo molto strano che ciò accada in un'app firmata distribuita sul dispositivo, ma non in una versione di debug distribuita da Eclipse. Rende abbastanza difficile il debug!
Matt Connolly

6
Questo non accade con una versione di debug distribuito da Eclipse fino a quando si avvia anche tramite Eclipse (o IntelliJ o altri IDE). Non ha nulla a che fare con il modo in cui l'app viene installata sul dispositivo. Il problema è dovuto al modo in cui l'app viene avviata .
David Wasser

2
Qualcuno sa se questo codice garantirà che l'istanza esistente dell'app venga portata in primo piano? Oppure chiama semplicemente finish (); e lasciare l'utente senza alcuna indicazione visiva che sia successo qualcosa?
Carlos P

5
@CarlosP se l'attività che viene creata non è l'attività radice dell'attività, deve esserci (per definizione) almeno un'altra attività al di sotto di essa. Se questa attività chiama, finish()l'utente vedrà l'attività che si trovava sotto. Per questo motivo puoi tranquillamente presumere che l'istanza esistente dell'app verrà portata in primo piano. In caso contrario, avresti più istanze dell'app in attività separate e l'attività da creare sarebbe la radice della sua attività.
David Wasser

27

Spiegherò solo perché fallisce e come riprodurre questo bug a livello di programmazione in modo da poterlo incorporare nella tua suite di test:

  1. Quando avvii un'app tramite Eclipse o Market App, viene avviata con i flag di intenzione: FLAG_ACTIVITY_NEW_TASK.

  2. Quando si avvia tramite il programma di avvio (home), utilizza i flag: FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_BROUGHT_TO_FRONT | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED e utilizza l'azione " MAIN " e la categoria " LAUNCHER ".

Se desideri riprodurlo in uno scenario di test, segui questi passaggi:

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

Quindi fai tutto il necessario per passare all'altra attività. Per i miei scopi, ho appena posizionato un pulsante che avvia un'altra attività. Quindi, torna al programma di avvio (home) con:

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

E simula il lancio tramite il programma di avvio con questo:

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

Se non hai incorporato la soluzione isTaskRoot (), questo riprodurrà il problema. Lo usiamo nei nostri test automatici per assicurarci che questo bug non si ripresenti mai più.

Spero che questo ti aiuti!


8

Hai provato la modalità di lancio singleTop ?

Ecco alcune delle descrizioni da http://developer.android.com/guide/topics/manifest/activity-element.html :

... una nuova istanza di un'attività "singleTop" può anche essere creata per gestire un nuovo intento. Tuttavia, se l'attività di destinazione ha già un'istanza esistente dell'attività in cima al suo stack, tale istanza riceverà il nuovo intento (in una chiamata onNewIntent ()); non viene creata una nuova istanza. In altre circostanze, ad esempio, se un'istanza esistente dell'attività "singleTop" si trova nell'attività di destinazione, ma non in cima allo stack, o se è in cima a uno stack, ma non nell'attività di destinazione - a nuova istanza verrebbe creata e inserita nello stack.


2
Ci ho pensato, ma cosa succede se l'attività non è in cima alla pila? Ad esempio, sembra che singleTop impedirà AA, ma non ABA.
bsberkeley

Puoi ottenere ciò che desideri utilizzando singleTop e i metodi finish all'interno di Activity?
Eric Levine

Non so se riuscirà a realizzare quello che voglio. Esempio: se sono in attività C dopo aver fatto scoppiare A e B, viene lanciata una nuova attività A e avrò qualcosa come CA, no?
bsberkeley

È difficile rispondere a questa domanda senza capire meglio cosa fanno queste attività. Potete fornire maggiori dettagli sulla vostra applicazione e sulle attività? Mi chiedo se ci sia una discrepanza tra ciò che fa il pulsante Home e come vuoi che agisca. Il pulsante home non esce da un'attività, la "fa da sfondo" in modo che l'utente possa passare a qualcos'altro. Il pulsante Indietro è ciò che esce / finisce e l'attività. Rompere questo paradigma potrebbe confondere / frustrare gli utenti.
Eric Levine

Ho aggiunto un'altra risposta a questo thread in modo da poter vedere una copia del manifest.
bsberkeley

4

Forse è questo problema ? O qualche altra forma dello stesso bug?


Vedi anche code.google.com/p/android/issues/detail?id=26658 , che dimostra che è causato da cose diverse da Eclipse.
Kristopher Johnson

1
Quindi dovrei copiare e incollare una descrizione del problema che potrebbe diventare obsoleta? Quali parti? Le parti essenziali devono essere conservate se il collegamento cambia, ed è mia responsabilità che la risposta sia aggiornata? Si dovrebbe pensare che il collegamento non sia più valido solo se il problema viene risolto. Questo non è un collegamento a un blog, dopotutto.
DuneCat

2

Penso che la risposta accettata ( Duane Homick ) non abbia gestito casi:

Hai diversi extra (e di conseguenza duplicati dell'app):

  • quando avvii l'applicazione da Market o tramite l'icona della schermata iniziale (che viene posizionata automaticamente da Market)
  • quando si avvia l'applicazione tramite il programma di avvio o l'icona della schermata iniziale creata manualmente

Ecco una soluzione (SDK_INT> = 11 per le notifiche) che credo gestisca anche questi casi e le notifiche della barra di stato.

Manifesto :

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

Attività di avvio :

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

Servizio :

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

Notifica :

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

2

Mi rendo conto che la domanda non ha nulla a che fare con Xamarin Android ma volevo postare qualcosa visto che non l'ho vista da nessun'altra parte.

Per risolvere questo problema in Xamarin Android, ho usato il codice da @DuaneHomick e aggiunto in MainActivity.OnCreate(). La differenza con Xamarin è che deve andare dopo Xamarin.Forms.Forms.Init(this, bundle);e LoadApplication(new App());. Quindi il mio OnCreate()sarebbe simile a:

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

* Modifica: a partire da Android 6.0, la soluzione di cui sopra non è sufficiente per determinate situazioni. Ora ho anche impostare LaunchModea SingleTask, che sembra aver fatto le cose funzionano correttamente ancora una volta. Non sono sicuro degli effetti che questo potrebbe avere su altre cose, purtroppo.


0

Ho avuto lo stesso problema e l'ho risolto utilizzando la seguente soluzione.

Nella tua attività principale aggiungi questo codice all'inizio del onCreatemetodo:

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

non dimenticare di aggiungere questa autorizzazione nel tuo manifest.

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

spero che ti aiuti.


0

Ho avuto anche questo problema

  1. Non chiamare finish (); nell'attività domestica sarebbe eseguita all'infinito: l'attività domestica viene chiamata da ActivityManager al termine.
  2. Di solito quando la configurazione sta cambiando (cioè ruota lo schermo, cambia lingua, cambia il servizio di telefonia cioè mcc mnc ecc.) L'attività ricrea - e se l'attività domestica è in esecuzione, chiama di nuovo A. per quella necessità di aggiungere al manifest android:configChanges="mcc|mnc"- se hai una connessione al cellulare, vedi http://developer.android.com/guide/topics/manifest/activity-element.html#config per quale configurazione c'è all'avvio del sistema o push open o qualsiasi altra cosa.

0

Prova questa soluzione:
crea una Applicationclasse e definisci lì:

public static boolean IS_APP_RUNNING = false;

Quindi nella tua prima attività (Launcher) onCreateprima di setContentView(...)aggiungere questo:

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

PS Controllerè la mia Applicationclasse.


Dovresti usare il booleano primitivo, che rende il controllo di null non necessario.
WonderCsabo

Questo non funzionerà sempre. Non sarai mai in grado di avviare la tua app, uscire dalla tua app e quindi riavviarla rapidamente. Android non interrompe necessariamente il processo di hosting del sistema operativo non appena non ci sono attività attive. In questo caso, quando si avvia l'applicazione di nuovo, la variabile IS_APP_RUNNINGsarà truee la vostra applicazione sarà immediatamente smettere. Non qualcosa che l'utente probabilmente troverà divertente.
David Wasser

-2

provare a utilizzare SingleInstance modalità di lancio con affinità set per allowtaskreparenting Questo creerà sempre l'attività nel nuovo compito, ma consentono anche la sua reparenting. Controlla l' attributo dis: Affinity


2
Probabilmente non funzionerà perché, secondo la documentazione, "la re-genitorialità è limitata alle modalità" standard "e" singleTop "." perché le "attività con modalità di avvio" singleTask "o" singleInstance "possono essere solo alla radice di un'attività"
bsberkeley

-2

Ho trovato un modo per evitare di iniziare le stesse attività, funziona benissimo per me

if ( !this.getClass().getSimpleName().equals("YourActivityClassName")) {
    start your activity
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.