Как предотвратить несколько экземпляров Activity, когда оно запускается с разными намерениями


121

Я обнаружил ошибку в своем приложении, когда оно запускается с помощью кнопки «Открыть» в приложении Google Play Store (ранее называвшемся Android Market). Кажется, что для его запуска из Play Store используется иное, Intentчем для запуска из меню значков приложений телефона. Это приводит к запуску нескольких копий одного и того же Activity, которые конфликтуют друг с другом.

Например, если мое приложение состоит из Activity ABC, то эта проблема может привести к стеку ABCA.

Я пробовал использовать android:launchMode="singleTask"все Activity, чтобы исправить эту проблему, но у него есть нежелательный побочный эффект очистки стека Activity до root, когда я нажимаю кнопку HOME.

Ожидаемое поведение: ABC -> HOME -> И когда приложение будет восстановлено, мне нужно: ABC -> HOME -> ABC

Есть ли хороший способ предотвратить запуск нескольких действий одного типа без возврата к корневому действию при использовании кнопки HOME?


Связанные тикеты в системе отслеживания ошибок Android: Issueetracker.google.com/issues/36941942 , Issueetracker.google.com/issues/36907463 , Issueetracker.google.com/issues/64108432
Mr-IDE,

Ответы:


187

Добавьте это в onCreate, и все будет в порядке:

// 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
Я пытался исправить эту ошибку в течение многих лет, и это решение сработало, так что большое спасибо! Я также должен отметить, что это проблема не только в Android Market, но и загрузка приложения неопубликованными приложениями путем его загрузки на сервер или отправки по электронной почте на свой телефон. Все эти вещи устанавливают приложение с помощью установщика пакетов, где, как мне кажется, и заключается ошибка. Кроме того, на случай, если это неясно, вам нужно только добавить этот код в метод onCreate того, что является вашей корневой активностью.
ubzack

2
Мне очень странно, что это происходит в подписанном приложении, развернутом на устройстве, но не в отладочной версии, развернутой из Eclipse. Делает отладку довольно сложной!
Мэтт Коннолли,

6
Это действительно происходит с отладочной версией, развернутой из Eclipse, если вы запускаете ее также через Eclipse (или IntelliJ, или другую IDE). Это не имеет ничего общего с тем, как приложение устанавливается на устройство. Проблема связана со способом запуска приложения .
Дэвид Вассер

2
Кто-нибудь знает, гарантирует ли этот код, что существующий экземпляр приложения будет выведен на передний план? Или он просто вызывает finish (); и оставить пользователя без визуальной индикации того, что что-то произошло?
Carlos P

5
@CarlosP, если создаваемое действие не является корневым действием задачи, под ним должно быть (по определению) хотя бы одно другое действие. Если это действие вызывает, finish()то пользователь увидит действие, которое было внизу. Из-за этого вы можете с уверенностью предположить, что существующий экземпляр приложения будет выведен на передний план. Если бы это было не так, у вас было бы несколько экземпляров приложения в отдельных задачах, и создаваемое действие было бы корнем его задачи.
Дэвид Вассер

27

Я просто собираюсь объяснить, почему это не удается, и как воспроизвести эту ошибку программно, чтобы вы могли включить ее в свой набор тестов:

  1. Когда вы запускаете приложение через Eclipse или Market App, оно запускается с флагами намерений: FLAG_ACTIVITY_NEW_TASK.

  2. При запуске через лаунчер (домашний) использует флаги: FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_BROUGHT_TO_FRONT | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED и использует действие « MAIN » и категорию « LAUNCHER ».

Если вы хотите воспроизвести это в тестовом примере, выполните следующие действия:

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

Затем сделайте все необходимое, чтобы перейти к другому занятию. Для моих целей я просто разместил кнопку, которая запускает другое действие. Затем вернитесь в панель запуска (домой) с помощью:

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

И смоделируйте запуск через пусковую установку с помощью этого:

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

Если вы не включили обходной путь isTaskRoot (), это воспроизведет проблему. Мы используем это в нашем автоматическом тестировании, чтобы убедиться, что эта ошибка больше никогда не повторится.

Надеюсь это поможет!


8

Вы пробовали режим запуска singleTop ?

Вот некоторые описания из http://developer.android.com/guide/topics/manifest/activity-element.html :

... также может быть создан новый экземпляр действия "singleTop" для обработки нового намерения. Однако, если целевая задача уже имеет существующий экземпляр действия на вершине своего стека, этот экземпляр получит новое намерение (в вызове onNewIntent ()); новый экземпляр не создается. В других обстоятельствах - например, если существующий экземпляр действия «singleTop» находится в целевой задаче, но не наверху стека, или если он находится на вершине стека, но не в целевой задаче - a новый экземпляр будет создан и помещен в стек.


2
Я подумал об этом, но что, если действие находится не в верхней части стека? Например, кажется, что singleTop предотвратит AA, но не ABA.
bsberkeley 03

Можете ли вы добиться того, чего хотите, используя методы singleTop и finish в Activity?
Эрик Левин

Не знаю, выполнит ли это то, что я хочу. Пример: если я выполняю действие C после появления A и B, то запускается новое действие A, и у меня будет что-то вроде CA, не так ли?
bsberkeley 03

Трудно ответить на этот вопрос, не понимая, что делают эти мероприятия. Не могли бы вы предоставить более подробную информацию о своем приложении и мероприятиях? Интересно, есть ли несоответствие между тем, что делает кнопка «Домой», и тем, как вы хотите, чтобы она действовала. Кнопка «Домой» не закрывает действие, а «фон», чтобы пользователь мог переключиться на что-нибудь еще. Кнопка возврата - это то, что выходит / заканчивается и активность. Нарушение этой парадигмы может запутать / расстроить пользователей.
Эрик Левин,

Я добавил еще один ответ в эту ветку, чтобы вы могли увидеть копию манифеста.
bsberkeley 03

4

Может, в этом проблема ? Или какая-то другая форма той же ошибки?


См. Также code.google.com/p/android/issues/detail?id=26658 , который демонстрирует, что это вызвано чем-то другим, кроме Eclipse.
Кристофер Джонсон

1
Итак, я должен скопировать и вставить описание проблемы, которая может устареть? Какие части? Следует ли сохранить основные части, если ссылка изменится, и я отвечаю за то, чтобы ответ был актуальным? Следует думать, что ссылка становится недействительной, только если проблема решена. В конце концов, это не ссылка на блог.
DuneCat

2

Я думаю, что в принятом ответе ( Дуэйн Хомик ) есть необработанные случаи:

У вас есть разные дополнения (и в результате дублируются приложения):

  • при запуске приложения из Маркета или по значку на главном экране (который размещается Маркетом автоматически)
  • при запуске приложения с помощью пусковой установки или созданного вручную значка главного экрана

Вот решение (SDK_INT> = 11 для уведомлений), которое, как я полагаю, обрабатывает эти случаи, а также уведомления в строке состояния.

Манифест :

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

Активность пусковой установки :

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

Сервис :

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

Уведомление :

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

Я понимаю, что вопрос не имеет ничего общего с Xamarin Android, но я хотел что-то опубликовать, поскольку нигде больше этого не видел.

Чтобы исправить это в Xamarin Android, я использовал код из @DuaneHomick и добавил в MainActivity.OnCreate(). Разница с Xamarin в том, что он должен идти после Xamarin.Forms.Forms.Init(this, bundle);и LoadApplication(new App());. Итак, мой OnCreate()будет выглядеть так:

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

* Изменить: начиная с Android 6.0 вышеуказанного решения недостаточно для определенных ситуаций. Я теперь также установить LaunchModeна SingleTask, который , кажется, сделал все правильно работать еще раз. К сожалению, не уверен, как это может повлиять на другие вещи.


0

У меня была такая же проблема, и я исправил ее, используя следующее решение.

В своей основной деятельности добавьте этот код поверх onCreateметода:

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

не забудьте добавить это разрешение в свой манифест.

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

надеюсь, это поможет вам.


0

У меня тоже была эта проблема

  1. Не вызывайте finish (); в домашней активности она будет выполняться бесконечно - домашняя активность вызывается ActivityManager, когда она завершается.
  2. Обычно при изменении конфигурации (например, поворот экрана, изменение языка, изменение службы телефонии, например, mcc mnc и т. Д.) Действие воссоздается - и если домашнее действие выполняется, оно снова обращается к A. для этого необходимо добавить в манифест android:configChanges="mcc|mnc"- если у вас есть соединение с сотовой связью, см. http://developer.android.com/guide/topics/manifest/activity-element.html#config, какая конфигурация используется при загрузке системы или при открытии, или что-то еще.

0

Попробуйте это решение:
создайте Applicationкласс и определите там:

public static boolean IS_APP_RUNNING = false;

Затем в вашем первом (Launcher) Activity, onCreateпрежде чем setContentView(...)добавить это:

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

PS Controllerэто мой Applicationкласс.


Вы должны использовать примитивное логическое значение, что делает проверку на нулевое значение ненужной.
WonderCsabo,

Это не всегда сработает. Вы никогда не сможете запустить свое приложение, выйти из него, а затем быстро запустить его снова. Android не обязательно прекращает процесс хостинга ОС, как только нет активных действий. В этом случае, когда вы снова запустите приложение, переменная IS_APP_RUNNINGбудет равна, trueи ваше приложение немедленно закроется. Это не то, что пользователь может найти забавным.
Дэвид Вассер,

-2

попробуйте использовать режим запуска SingleInstance с параметром affinity, установленным на allowtaskreparenting. Это всегда будет создавать действие в новой задаче, но также разрешит его повторное создание. Проверить dis: Атрибут сходства


2
Наверное, не сработает, потому что, согласно документации, «повторное родительство ограничено режимами« стандартный »и« singleTop ». потому что «действия с режимами запуска singleTask» или «singleInstance» могут быть только в основе задачи »
bsberkeley

-2

Я нашел способ не запускать одни и те же действия, это отлично работает для меня

if ( !this.getClass().getSimpleName().equals("YourActivityClassName")) {
    start your activity
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.