Como evitar várias instâncias de uma atividade quando ela é iniciada com diferentes intenções

121

Encontrei um bug no meu aplicativo quando ele foi iniciado usando o botão "Abrir" no aplicativo Google Play Store (anteriormente chamado Android Market). Parece que iniciá-lo na Play Store usa um modo diferente do Intentque iniciá-lo no menu de ícones do aplicativo do telefone. Isso leva ao lançamento de várias cópias da mesma atividade, que estão em conflito entre si.

Por exemplo, se meu aplicativo consistir nas Atividades ABC, esse problema poderá levar a uma pilha de ABCA.

Tentei usar android:launchMode="singleTask"todas as atividades para corrigir esse problema, mas ele tem o efeito colateral indesejado de limpar a pilha de atividades para fazer root, sempre que clico no botão HOME.

O comportamento esperado é: ABC -> HOME -> E quando o aplicativo é restaurado, preciso: ABC -> HOME -> ABC

Existe uma boa maneira de impedir o lançamento de várias atividades do mesmo tipo, sem redefinir a atividade raiz ao usar o botão HOME?

bsberkeley
fonte

Respostas:

187

Adicione isso ao onCreate e você deve começar:

// 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
fonte
25
Eu venho tentando resolver esse bug há anos, e essa foi a solução que funcionou, então muito obrigado! Também preciso observar que isso não é apenas um problema no Android Market, mas também o carregamento lateral de um aplicativo, enviando-o para um servidor ou enviando-o por e-mail para o telefone, causa esse problema. Todas essas coisas instalam o aplicativo usando o Instalador de Pacotes, onde acredito que o bug reside. Além disso, caso não esteja claro, você só precisará adicionar esse código ao método onCreate de qual é a sua atividade raiz.
ubzack
2
Acho muito estranho que isso aconteça em um aplicativo assinado implantado no dispositivo, mas não em uma versão de depuração implantada no Eclipse. Torna bastante difícil depurar!
precisa saber é o seguinte
6
Isso não acontece com uma versão de depuração implantado a partir de Eclipse, desde que você iniciá-lo também via Eclipse (ou IntelliJ ou outro IDE). Não tem nada a ver com a forma como o aplicativo é instalado no dispositivo. O problema é devido à maneira como o aplicativo é iniciado .
David Wasser
2
Alguém sabe se esse código garantirá que a instância existente do aplicativo seja trazida para o primeiro plano? Ou apenas chama terminar (); e deixar o usuário sem indicação visual de que algo aconteceu?
Carlos P
5
@CarlosP se a atividade que está sendo criada não for a atividade raiz da tarefa, deve (por definição) haver pelo menos uma outra atividade abaixo dela. Se essa atividade chamar finish(), o usuário verá a atividade que estava por baixo. Por isso, você pode assumir com segurança que a instância existente do aplicativo será trazida para o primeiro plano. Se não fosse esse o caso, você teria várias instâncias do aplicativo em tarefas separadas e a atividade criada seria a raiz de sua tarefa.
David Wasser
27

Vou apenas explicar por que ele falha e como reproduzir esse bug programaticamente para que você possa incorporar isso ao seu conjunto de testes:

  1. Quando você inicia um aplicativo por meio do Eclipse ou do Market App, ele é iniciado com sinalizadores de intenção: FLAG_ACTIVITY_NEW_TASK.

  2. Ao iniciar pelo iniciador (home), ele usa sinalizadores: FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_BROUGHT_TO_FRONT | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED e usa a ação " MAIN " e a categoria " LAUNCHER ".

Se você deseja reproduzir isso em um caso de teste, siga estas etapas:

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

Em seguida, faça o que for necessário para chegar à outra atividade. Para meus propósitos, acabei de colocar um botão que inicia outra atividade. Em seguida, volte ao iniciador (casa) com:

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

E simule o lançamento através do iniciador com este:

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

Se você não incorporou a solução alternativa isTaskRoot (), isso reproduzirá o problema. Usamos isso em nossos testes automáticos para garantir que esse bug nunca ocorra novamente.

Espero que isto ajude!

gilm
fonte
8

Você já experimentou o modo de inicialização singleTop ?

Aqui estão algumas das descrições em http://developer.android.com/guide/topics/manifest/activity-element.html :

... uma nova instância de uma atividade "singleTop" também pode ser criada para lidar com uma nova intenção. No entanto, se a tarefa de destino já tiver uma instância existente da atividade no topo de sua pilha, essa instância receberá a nova intenção (em uma chamada onNewIntent ()); uma nova instância não é criada. Em outras circunstâncias - por exemplo, se uma instância existente da atividade "singleTop" estiver na tarefa de destino, mas não no topo da pilha, ou se estiver no topo de uma pilha, mas não na tarefa de destino - a nova instância seria criada e empurrada na pilha.

Eric Levine
fonte
2
Pensei nisso, mas e se a atividade não estiver no topo da pilha? Por exemplo, parece que o singleTop impedirá o AA, mas não o ABA.
precisa saber é o seguinte
Você consegue o que deseja usando o singleTop e os métodos de acabamento no Activity?
Eric Levine
Não sei se conseguirá o que quero. Exemplo: se estou na atividade C depois de exibir A e B, uma nova atividade A é iniciada e terei algo como CA, não é?
precisa saber é o seguinte
É difícil responder a isso sem entender mais sobre o que essas atividades fazem. Você pode fornecer mais detalhes sobre seu aplicativo e as atividades? Gostaria de saber se existe uma incompatibilidade entre o que o botão Início faz e como você deseja que ele atue. O botão home não sai de uma atividade, ele o "embandeira" para que o usuário possa mudar para outra coisa. O botão Voltar é o que sai / termina e atividade. Quebrar esse paradigma pode confundir / frustrar os usuários.
Eric Levine
Adicionei outra resposta a este tópico para que você possa ver uma cópia do manifesto.
precisa
4

Talvez seja esse problema ? Ou alguma outra forma do mesmo bug?

DuneCat
fonte
Consulte também code.google.com/p/android/issues/detail?id=26658 , que demonstra que é causado por outras coisas que não o Eclipse.
21813 Kristopher Johnson
1
Então, devo copiar e colar uma descrição do problema que possa ficar obsoleta? Quais partes? As partes essenciais devem ser mantidas se o link mudar, e é minha responsabilidade que a resposta seja mantida atualizada? Deve-se pensar que o link só se torna inválido se o problema for resolvido. Afinal, este não é um link para um blog.
DuneCat
2

Eu acho que a resposta aceita ( Duane Homick ) tem casos não tratados :

Você tem extras diferentes (e o aplicativo duplicou como resultado):

  • quando você inicia o aplicativo no Market ou pelo ícone da tela inicial (colocado automaticamente pelo Market)
  • quando você inicia o aplicativo pelo iniciador ou pelo ícone da tela inicial criado manualmente

Aqui está uma solução (SDK_INT> = 11 para notificações) que eu acredito lidar com esses casos e notificações da barra de status também.

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

Atividade do iniciador :

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

Serviço :

@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ção :

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

Percebo que a pergunta não tem nada a ver com o Xamarin Android, mas eu queria postar algo, já que não o vi em nenhum outro lugar.

Para corrigir isso no Xamarin Android, usei o código de @DuaneHomick e o adicionei MainActivity.OnCreate(). A diferença com o Xamarin é que é preciso ir atrás Xamarin.Forms.Forms.Init(this, bundle);e LoadApplication(new App());. Então, minha OnCreate()aparência seria:

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 o Android 6.0, a solução acima não é suficiente para determinadas situações. Agora também defini LaunchModecomo SingleTask, o que parece ter feito as coisas funcionarem corretamente mais uma vez. Não tenho certeza de que efeitos isso pode ter em outras coisas, embora infelizmente.

hvaughan3
fonte
0

Eu também tive esse problema

  1. Não chame finish (); na atividade em casa, ela seria executada infinitamente - a atividade em casa é chamada pelo ActivityManager quando é concluída.
  2. Normalmente, quando a configuração está mudando (por exemplo, girar a tela, alterar o idioma, o serviço de telefonia muda, por exemplo, mcc mnc etc.), a atividade é recriada - e se a atividade em casa estiver em execução, ela chama novamente a A. para que seja necessário adicionar o manifesto android:configChanges="mcc|mnc"- se você tem conexão com celular, consulte http://developer.android.com/guide/topics/manifest/activity-element.html#config para qual configuração existe ao inicializar o sistema ou empurrar para abrir ou o que for.
user1249350
fonte
0

Tente esta solução:
Crie uma Applicationclasse e defina lá:

public static boolean IS_APP_RUNNING = false;

Em seguida, na sua primeira atividade (iniciador) onCreateantes de setContentView(...)adicionar este:

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

PS Controlleré minha Applicationclasse.

Volodymyr Kulyk
fonte
Você deve usar booleano primitivo, o que torna a verificação de nulos desnecessários.
WonderCsabo
Isso nem sempre funciona. Você nunca seria capaz de iniciar seu aplicativo, sair do aplicativo e iniciá-lo rapidamente novamente. O Android não elimina necessariamente o processo do sistema operacional de hospedagem assim que não há atividades ativas. Nesse caso, quando você iniciar o aplicativo novamente, a variável IS_APP_RUNNINGserá truee seu aplicativo será encerrado imediatamente. Não é algo que o usuário possa achar divertido.
David Wasser
-1

Eu tive o mesmo problema e o corrigi usando a seguinte solução.

Na sua atividade principal, adicione este código na parte superior do 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();
    }
}

não esqueça de adicionar essa permissão no seu manifesto.

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

espero que ajude você.

gugarush
fonte
-2

tente usar o modo de inicialização SingleInstance com afinidade definida como allowtaskreparenting Isso sempre criará a atividade em uma nova tarefa, mas também permitirá sua reparação. Marque dis: atributo Affinity

Shaireen
fonte
2
Provavelmente não funcionará porque, de acordo com a documentação "a reorientação é limitada aos modos" padrão "e" singleTop "." porque "atividades com 'SingleInstance singleTask' ou '' modos de lançamento só pode estar na raiz de uma tarefa"
bsberkeley
-2

Eu encontrei uma maneira de impedir o início das mesmas atividades, isso funciona muito bem para mim

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