Jak zapobiegać wielu wystąpieniom działania, gdy jest ono uruchamiane z różnymi intencjami

121

Znalazłem błąd w mojej aplikacji, gdy jest ona uruchamiana za pomocą przycisku „Otwórz” w aplikacji Sklep Google Play (poprzednio nazywanej Android Market). Wygląda na to, że uruchomienie go ze Sklepu Play działa inaczej Intentniż uruchomienie go z menu ikon aplikacji telefonu. Prowadzi to do uruchamiania wielu kopii tego samego działania, które są ze sobą w konflikcie.

Na przykład, jeśli moja aplikacja składa się z ABC działań, ten problem może prowadzić do stosu ABCA.

Próbowałem użyć android:launchMode="singleTask"we wszystkich działaniach, aby rozwiązać ten problem, ale ma to niepożądany efekt uboczny polegający na wyczyszczeniu stosu działań do katalogu głównego, za każdym razem, gdy naciskam przycisk HOME.

Oczekiwane zachowanie to: ABC -> HOME -> A kiedy aplikacja zostanie przywrócona, potrzebuję: ABC -> HOME -> ABC

Czy istnieje dobry sposób, aby zapobiec uruchamianiu wielu działań tego samego typu bez konieczności resetowania do działania głównego podczas korzystania z przycisku HOME?

bsberkeley
źródło
Powiązane bilety w narzędziu do śledzenia błędów Androida: issuetracker.google.com/issues/36941942 , issuetracker.google.com/issues/36907463 , issuetracker.google.com/issues/64108432
Mr-IDE

Odpowiedzi:

187

Dodaj to do onCreate i powinieneś być gotowy:

// 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
źródło
25
Od lat próbuję rozwiązać ten błąd i to było rozwiązanie, które zadziałało, więc bardzo dziękuję! Muszę również zauważyć, że jest to nie tylko problem w Android Market, ale także ładowanie aplikacji przez przesłanie jej na serwer lub wysłanie jej e-mailem na telefon powoduje ten problem. Wszystkie te rzeczy instalują aplikację za pomocą Instalatora pakietów, w którym, jak sądzę, znajduje się błąd. Ponadto, na wypadek, gdyby nie było to jasne, wystarczy dodać ten kod do metody onCreate, aby określić, jaka jest Twoja aktywność roota.
ubzack
2
Wydaje mi się bardzo dziwne, że dzieje się tak w podpisanej aplikacji wdrożonej na urządzeniu, ale nie w wersji do debugowania wdrożonej z Eclipse. Sprawia, że ​​debugowanie jest dość trudne!
Matt Connolly,
6
Dzieje się tak w przypadku wersji debugowania wdrożonej z Eclipse, o ile URUCHOMISZ ją również za pomocą Eclipse (lub IntelliJ lub innego IDE). To nie ma nic wspólnego z tym, jak aplikacja zostanie zainstalowana na urządzeniu. Problemem jest to, ze względu na sposób, w jaki aplikacja jest rozpoczął .
David Wasser
2
Czy ktoś wie, czy ten kod zapewni, że istniejące wystąpienie aplikacji zostanie przeniesione na pierwszy plan? A może po prostu wywołuje finish (); i pozostawić użytkownika bez wizualnego wskazania, że ​​coś się stało?
Carlos P
5
@CarlosP Jeśli tworzone działanie nie jest głównym działaniem zadania, musi (z definicji) znajdować się pod nim co najmniej jedno inne działanie. Jeśli to działanie finish()wywoła, użytkownik zobaczy działanie, które znajdowało się pod spodem. Dzięki temu można spokojnie założyć, że istniejąca instancja aplikacji zostanie przeniesiona na pierwszy plan. Gdyby tak nie było, miałbyś wiele wystąpień aplikacji w oddzielnych zadaniach, a tworzona aktywność byłaby źródłem jej zadania.
David Wasser
27

Wyjaśnię tylko, dlaczego to się nie udaje i jak programowo odtworzyć ten błąd, abyś mógł włączyć go do swojego zestawu testów:

  1. Kiedy uruchamiasz aplikację za pośrednictwem Eclipse lub Market, uruchamia się ona z flagami intencji: FLAG_ACTIVITY_NEW_TASK.

  2. Podczas uruchamiania za pomocą programu uruchamiającego (strona główna) używa flag: FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_BROUGHT_TO_FRONT | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED i używa akcji „ MAIN ” i kategorii „ LAUNCHER ”.

Jeśli chcesz to odtworzyć w przypadku testowym, wykonaj następujące kroki:

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

Następnie zrób wszystko, co konieczne, aby przejść do innej czynności. Dla moich celów właśnie umieściłem przycisk, który uruchamia inną czynność. Następnie wróć do programu uruchamiającego (strona główna) za pomocą:

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

I zasymuluj uruchomienie go za pomocą programu uruchamiającego za pomocą tego:

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

Jeśli nie zastosowałeś obejścia isTaskRoot (), spowoduje to odtworzenie problemu. Używamy tego w naszych automatycznych testach, aby upewnić się, że ten błąd nigdy się nie powtórzy.

Mam nadzieję że to pomoże!

gilm
źródło
8

Czy wypróbowałeś tryb uruchamiania singleTop ?

Oto część opisu z http://developer.android.com/guide/topics/manifest/activity-element.html :

... można również utworzyć nową instancję działania „singleTop” do obsługi nowej intencji. Jeśli jednak zadanie docelowe ma już istniejącą instancję działania na szczycie swojego stosu, instancja ta otrzyma nową intencję (w wywołaniu onNewIntent ()); nowa instancja nie jest tworzona. W innych okolicznościach - na przykład, jeśli istniejąca instancja działania „singleTop” znajduje się w zadaniu docelowym, ale nie znajduje się na szczycie stosu, lub jeśli znajduje się na szczycie stosu, ale nie w zadaniu docelowym - a nowa instancja zostanie utworzona i umieszczona na stosie.

Eric Levine
źródło
2
Pomyślałem o tym, ale co, jeśli aktywność nie znajduje się na szczycie stosu? Na przykład wygląda na to, że singleTop zapobiegnie AA, ale nie ABA.
bsberkeley
Czy możesz osiągnąć to, co chcesz, używając metod singleTop i finish w ramach Activity?
Eric Levine,
Nie wiem, czy uda mi się to osiągnąć. Przykład: Jeśli jestem w działaniu C po pojawieniu się A i B, zostanie uruchomione nowe działanie A i będę miał coś takiego jak CA, prawda?
bsberkeley
Trudno odpowiedzieć na to pytanie, nie rozumiejąc więcej tego, co robią te czynności. Czy możesz podać więcej szczegółów na temat swojej aplikacji i działań? Zastanawiam się, czy istnieje niedopasowanie między tym, co robi przycisk Home, a tym, jak chcesz, aby działał. Przycisk strony głównej nie zamyka działania, tylko je „umieszcza w tle”, aby użytkownik mógł przełączyć się na coś innego. Przycisk Wstecz jest tym, co wychodzi / kończy i działa. Złamanie tego paradygmatu może dezorientować / frustrować użytkowników.
Eric Levine
Dodałem kolejną odpowiedź do tego wątku, abyś mógł zobaczyć kopię manifestu.
bsberkeley
4

Może to jest ten problem ? Lub inna forma tego samego błędu?

DuneCat
źródło
Zobacz też code.google.com/p/android/issues/detail?id=26658 , który pokazuje, że jest to spowodowane czynnikami innymi niż Eclipse.
Kristopher Johnson,
1
Powinienem więc skopiować i wkleić opis problemu, który może się zestarzeć? Które części? Czy w przypadku zmiany linku należy zachować najważniejsze części i czy odpowiadam za aktualizowanie odpowiedzi? Należy pomyśleć, że link traci ważność tylko wtedy, gdy problem zostanie rozwiązany. W końcu to nie jest link do bloga.
DuneCat
2

Myślę, że zaakceptowana odpowiedź ( Duane Homick ) ma nieobsłużone przypadki:

Masz różne dodatki (i w rezultacie duplikaty aplikacji):

  • po uruchomieniu aplikacji z Market lub przez ikonę na ekranie głównym (która jest umieszczana automatycznie przez Market)
  • po uruchomieniu aplikacji za pomocą programu uruchamiającego lub ręcznie utworzonej ikony ekranu głównego

Oto rozwiązanie (SDK_INT> = 11 dla powiadomień), które, jak sądzę, obsługuje również te przypadki i powiadomienia na pasku stanu.

Manifest :

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

Aktywność programu uruchamiającego :

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

Usługa :

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

Powiadomienie :

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
źródło
2

Zdaję sobie sprawę, że pytanie nie ma nic wspólnego z Xamarinem na Androida, ale chciałem coś opublikować, ponieważ nigdzie indziej tego nie widziałem.

Aby rozwiązać ten problem w Xamarin Android, użyłem kodu z @DuaneHomick i dodałem do MainActivity.OnCreate(). Różnica w porównaniu z platformą Xamarin polega na tym, że należy postępować po Xamarin.Forms.Forms.Init(this, bundle);i LoadApplication(new App());. Więc mój OnCreate()wyglądałby tak:

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

* Edycja: od Androida 6.0 powyższe rozwiązanie nie jest wystarczające w niektórych sytuacjach. Ustawiłem teraz również LaunchModena SingleTask, co wydaje się sprawiać, że wszystko znów działa poprawnie. Niestety nie jestem pewien, jaki wpływ może to mieć na inne rzeczy.

hvaughan3
źródło
0

Miałem ten sam problem i naprawiłem go za pomocą następującego rozwiązania.

W swojej głównej aktywności dodaj ten kod na górze onCreatemetody:

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

nie zapomnij dodać tego uprawnienia w swoim manifeście.

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

mam nadzieję, że ci to pomoże.

gugarush
źródło
0

Ja też miałem ten problem

  1. Nie wywołuj finish (); w przypadku aktywności domowej działałaby bez końca - aktywność domowa jest wywoływana przez ActivityManager po jej zakończeniu.
  2. Zwykle, gdy zmienia się konfiguracja (np. Obraca ekran, zmienia język, zmienia się usługa telefoniczna, np. Mcc mnc itp.), Czynność jest odtwarzana - i jeśli aktywność domowa jest uruchomiona, ponownie woła do A. w celu dodania do manifestu android:configChanges="mcc|mnc"- jeśli masz połączenie z siecią komórkową, zobacz http://developer.android.com/guide/topics/manifest/activity-element.html#config, dla której konfiguracji występuje podczas uruchamiania systemu, otwierania lub czegokolwiek innego.
user1249350
źródło
0

Wypróbuj to rozwiązanie:
utwórz Applicationklasę i zdefiniuj w niej:

public static boolean IS_APP_RUNNING = false;

Następnie w swojej pierwszej (Launcher) aktywności onCreateprzed setContentView(...)dodaniem tego:

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

PS Controllerto moja Applicationklasa.

Volodymyr Kulyk
źródło
Powinieneś używać prymitywnych wartości boolowskich, co sprawia, że ​​sprawdzanie wartości null nie jest konieczne.
WonderCsabo
To nie zawsze będzie działać. Nigdy nie byłbyś w stanie uruchomić swojej aplikacji, zamknąć aplikacji, a następnie szybko ponownie uruchomić aplikację. Android niekoniecznie zabija proces hostingu systemu operacyjnego, gdy tylko nie ma aktywnych działań. W takim przypadku, gdy ponownie uruchomisz aplikację, zmienna IS_APP_RUNNINGbędzie truei Twoja aplikacja natychmiast się zakończy. Nie jest to coś, co użytkownik mógłby uznać za zabawne.
David Wasser
-2

spróbuj użyć trybu uruchamiania SingleInstance z powinowactwem ustawionym na allowtaskreparenting. Spowoduje to zawsze utworzenie działania w nowym zadaniu, ale także umożliwi jego ponowne rodzicielstwo. Sprawdź atrybut dis: Affinity

Shaireen
źródło
2
Prawdopodobnie nie zadziała, ponieważ zgodnie z dokumentacją „ponowne rodzicielstwo jest ograniczone do trybów„ standard ”i„ singleTop ”. ponieważ „działania z trybami uruchamiania„ singleTask ”lub„ singleInstance ”mogą znajdować się tylko w katalogu głównym zadania”
bsberkeley,
-2

Znalazłem sposób, aby zapobiec rozpoczynaniu tych samych czynności, to działa świetnie

if ( !this.getClass().getSimpleName().equals("YourActivityClassName")) {
    start your activity
}
Odhik Susanto
źródło