Cómo prevenir múltiples instancias de una actividad cuando se inicia con diferentes intenciones

121

Me he encontrado con un error en mi aplicación cuando se inicia con el botón "Abrir" en la aplicación Google Play Store (anteriormente llamada Android Market). Parece que lanzarlo desde Play Store utiliza una forma diferente Intentde hacerlo desde el menú de iconos de la aplicación del teléfono. Esto lleva a que se inicien varias copias de la misma actividad, que están en conflicto entre sí.

Por ejemplo, si mi aplicación consta de Actividades ABC, entonces este problema puede generar una pila de ABCA.

Intenté usar android:launchMode="singleTask" en todas las actividades para solucionar este problema, pero tiene el efecto secundario no deseado de borrar la pila de actividades a la raíz cada vez que presiono el botón INICIO.

El comportamiento esperado es: ABC -> INICIO -> Y cuando se restaura la aplicación, necesito: ABC -> INICIO -> ABC

¿Existe una buena manera de evitar que se inicien varias actividades del mismo tipo, sin restablecer la actividad raíz cuando se usa el botón INICIO?

bsberkeley
fuente

Respuestas:

187

Agregue esto a onCreate y debería estar listo para comenzar:

// 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
fuente
25
He estado tratando de resolver este error durante años y esta fue la solución que funcionó, ¡así que muchas gracias! También debo señalar que esto no es solo un problema dentro de Android Market, sino que también la descarga de una aplicación cargándola en un servidor o enviándola por correo electrónico a su teléfono, causa este problema. Todas estas cosas instalan la aplicación usando el Instalador de paquetes, donde creo que reside el error. Además, en caso de que no esté claro, solo necesita agregar este código al método onCreate de cuál es su actividad raíz.
ubzack
2
Me parece muy extraño que esto suceda en una aplicación firmada implementada en el dispositivo, pero no en una versión de depuración implementada desde Eclipse. ¡Hace que sea bastante difícil de depurar!
Matt Connolly
6
Esto no sucede con una versión de depuración desplegado desde Eclipse, siempre y cuando se inicia también a través de Eclipse (o IntelliJ u otro IDE). No tiene nada que ver con cómo se instala la aplicación en el dispositivo. El problema se debe a la forma en que se inicia la aplicación .
David Wasser
2
¿Alguien sabe si este código garantizará que la instancia existente de la aplicación pase a primer plano? ¿O simplemente llama a finish (); y dejar al usuario sin indicación visual de que ha sucedido algo?
Carlos P
5
@CarlosP si la actividad que se está creando no es la actividad raíz de la tarea, debe haber (por definición) al menos otra actividad debajo de ella. Si esta actividad llama finish(), el usuario verá la actividad que estaba debajo. Por eso, puede asumir con seguridad que la instancia existente de la aplicación pasará a primer plano. Si ese no fuera el caso, tendría varias instancias de la aplicación en tareas separadas y la actividad que se está creando sería la raíz de su tarea.
David Wasser
27

Solo voy a explicar por qué falla y cómo reproducir este error de manera programática para que pueda incorporar esto en su conjunto de pruebas:

  1. Cuando inicia una aplicación a través de Eclipse o Market App, se inicia con indicadores de intención: FLAG_ACTIVITY_NEW_TASK.

  2. Cuando se lanza a través del lanzador (inicio), usa banderas: FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_BROUGHT_TO_FRONT | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED, y usa la acción " MAIN " y la categoría " LAUNCHER ".

Si desea reproducir esto en un caso de prueba, siga estos pasos:

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

Luego haga lo que sea necesario para llegar a la otra actividad. Para mis propósitos, acabo de colocar un botón que inicia otra actividad. Luego, regrese al lanzador (inicio) con:

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

Y simule lanzarlo a través del lanzador con esto:

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

Si no ha incorporado la solución alternativa isTaskRoot (), esto reproducirá el problema. Usamos esto en nuestras pruebas automáticas para asegurarnos de que este error nunca vuelva a ocurrir.

¡Espero que esto ayude!

Gilm
fuente
8

¿Has probado el singleTop? modo de lanzamiento ?

Aquí está parte de la descripción de http://developer.android.com/guide/topics/manifest/activity-element.html :

... también se puede crear una nueva instancia de una actividad "singleTop" para manejar una nueva intención. Sin embargo, si la tarea de destino ya tiene una instancia existente de la actividad en la parte superior de su pila, esa instancia recibirá la nueva intención (en una llamada onNewIntent ()); no se crea una nueva instancia. En otras circunstancias, por ejemplo, si una instancia existente de la actividad "singleTop" está en la tarea de destino, pero no en la parte superior de la pila, o si está en la parte superior de una pila, pero no en la tarea de destino, un Se creará una nueva instancia y se enviará a la pila.

Eric Levine
fuente
2
Pensé en eso, pero ¿qué pasa si la actividad no está en la parte superior de la pila? Por ejemplo, parece que singleTop evitará AA, pero no ABA.
bsberkeley
¿Puede lograr lo que desea utilizando singleTop y los métodos de acabado dentro de Activity?
Eric Levine
No sé si logrará lo que quiero. Ejemplo: si estoy en la actividad C después de seleccionar A y B, entonces se lanza una nueva actividad A y tendré algo como CA, ¿no?
bsberkeley
Es difícil responder a esto sin comprender más sobre lo que hacen estas actividades. ¿Puede proporcionar más detalles sobre su aplicación y las actividades? Me pregunto si hay una discrepancia entre lo que hace el botón Inicio y cómo quieres que actúe. El botón de inicio no sale de una actividad, la "pone de fondo" para que el usuario pueda cambiar a otra cosa. El botón de retroceso es lo que sale / termina y actividad. Romper ese paradigma podría confundir / frustrar a los usuarios.
Eric Levine
Agregué otra respuesta a este hilo para que pueda ver una copia del manifiesto.
bsberkeley
4

¿Quizás sea este problema ? ¿O alguna otra forma del mismo error?

DuneCat
fuente
Consulte también code.google.com/p/android/issues/detail?id=26658 , que demuestra que es causado por cosas distintas de Eclipse.
Kristopher Johnson
1
Entonces, ¿debería copiar y pegar una descripción del problema que podría volverse obsoleta? ¿Qué partes? ¿Deben conservarse las partes esenciales si cambia el enlace, y es mi responsabilidad que la respuesta se mantenga actualizada? Uno debería pensar que el enlace solo se vuelve inválido si se resuelve el problema. Después de todo, este no es un enlace a un blog.
DuneCat
2

Creo que la respuesta aceptada ( Duane Homick ) tiene casos sin :

Tienes diferentes extras (y duplicados de aplicaciones como resultado):

  • cuando inicia la aplicación desde Market o mediante el icono de la pantalla de inicio (que Market coloca automáticamente)
  • cuando inicia la aplicación mediante el iniciador o el icono de pantalla de inicio creado manualmente

Aquí hay una solución (SDK_INT> = 11 para notificaciones) que creo que maneja estos casos y las notificaciones de la barra de estado también.

Manifiesto :

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

Actividad del lanzador :

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

Servicio :

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

Notificacion :

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

Me doy cuenta de que la pregunta no tiene nada que ver con Xamarin Android pero quería publicar algo ya que no lo vi en ningún otro lado.

Para solucionar esto en Xamarin Android, usé el código de @DuaneHomick y lo agregué a MainActivity.OnCreate(). La diferencia con Xamarin es que debe ir tras Xamarin.Forms.Forms.Init(this, bundle);y LoadApplication(new App());. Entonces mi OnCreate()se vería así:

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

* Editar: Desde Android 6.0, la solución anterior no es suficiente para ciertas situaciones. He ahora también fijar LaunchModea SingleTask, que parece haber hecho las cosas funcionan correctamente una vez más. Desafortunadamente, no estoy seguro de qué efectos podría tener esto en otras cosas.

hvaughan3
fuente
0

Tuve el mismo problema y lo solucioné usando la siguiente solución.

En su actividad principal, agregue este código en la parte superior del onCreatemétodo:

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

no olvide agregar este permiso en su manifiesto.

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

Espero que te ayude.

Gugarush
fuente
0

Tuve este problema tambien

  1. No llames a terminar (); en la actividad doméstica se ejecutaría sin cesar; ActivityManager llama a la actividad doméstica cuando finaliza.
  2. Por lo general, cuando la configuración está cambiando (es decir, rotar la pantalla, cambiar el idioma, cambiar el servicio de telefonía, es decir, mcc mnc, etc.), la actividad se vuelve a crear, y si la actividad en el hogar se está ejecutando, vuelve a llamar a A. para esa necesidad de agregar al manifiesto android:configChanges="mcc|mnc", si tiene conexión a celular, consulte http://developer.android.com/guide/topics/manifest/activity-element.html#config para saber qué configuración hay al arrancar el sistema o presionar para abrir o lo que sea.
usuario1249350
fuente
0

Pruebe esta solución:
cree una Applicationclase y defina allí:

public static boolean IS_APP_RUNNING = false;

Luego, en su primera actividad (Lanzador) onCreateantes de setContentView(...)agregar esto:

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

PD Controlleres mi Applicationclase.

Volodymyr Kulyk
fuente
Debe utilizar un booleano primitivo, lo que hace que la comprobación de valores nulos sea innecesaria.
WonderCsabo
Esto no siempre funcionará. Nunca podrá iniciar su aplicación, salir de su aplicación y luego iniciar rápidamente su aplicación nuevamente. Android no necesariamente acaba con el proceso del sistema operativo de alojamiento tan pronto como no hay actividades activas. En este caso, cuando inicie la aplicación nuevamente, la variable IS_APP_RUNNINGserá truey su aplicación se cerrará inmediatamente. No es algo que el usuario pueda encontrar divertido.
David Wasser
-2

intente usar el modo de inicio de SingleInstance con afinidad establecida en allowtaskreparenting. Esto siempre creará la actividad en una nueva tarea, pero también permitirá su reparenting. Comprobar dis: atributo de afinidad

Shaireen
fuente
2
Probablemente no funcionará porque, según la documentación, "la re-crianza se limita a los modos" estándar "y" singleTop "". porque las "actividades con los modos de inicio" singleTask "o" singleInstance "solo pueden estar en la raíz de una tarea"
bsberkeley
-2

Encontré una manera de evitar comenzar las mismas actividades, esto funciona muy bien para mí

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