Как определить, когда приложение Android переходит в фоновый режим и возвращается на передний план - PullRequest
353 голосов
/ 11 декабря 2010

Я пытаюсь написать приложение, которое делает что-то конкретное, когда оно возвращается на передний план через некоторое время.Есть ли способ определить, когда приложение отправляется в фоновый режим или выводится на передний план?

Ответы [ 38 ]

177 голосов
/ 12 ноября 2013

2018: Android поддерживает это изначально с помощью компонентов жизненного цикла.

Март 2018 ОБНОВЛЕНИЕ : теперь есть лучшее решение.См. ProcessLifecycleOwner .Вам нужно будет использовать новые компоненты архитектуры 1.1.0 (самые последние на данный момент), но он специально предназначен для этого.

В этом ответе есть простой пример , но я написал пример приложения и сообщение в блоге об этом.

С тех пор, как я написал это в 2014 году, возникли разные решения.Некоторые работали, некоторые считали работающими , но имели недостатки (включая мою!), И мы, как сообщество (Android), научились справляться с последствиями и писали обходные пути для особых случаев.

Никогда не думайте, что единственный фрагмент кода - это решение, которое вы ищете, это маловероятно;еще лучше, попытайтесь понять, что он делает и почему он это делает.

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

Если только у вас нет веской причины не использовать компоненты новой архитектуры (а некоторые есть, особенно если вы используете супер старые apis), тогда используйте их.Они далеки от совершенства, но ни один из них не был ComponentCallbacks2.

ОБНОВЛЕНИЕ / ЗАМЕТКИ (ноябрь 2015 г.) : Люди делали два комментария, во-первых, вместо этого следует использовать >===, поскольку в документации указано, что не следует проверять точные значения .Это нормально для большинства случаев, но имейте в виду, что если вы только хотите сделать что-то , когда приложение перешло в фоновый режим, вам придется использовать == и также объедините его с другим решением (например, обратными вызовами Activity Lifecycle), иначе вы можете не получить желаемого эффекта.Пример (и это случилось со мной) заключается в том, что если вы хотите заблокировать ваше приложение с экраном пароля, когда оно переходит на задний план (например, 1Password, если вы знакомы с ним), вы можете случайно заблокироватьваше приложение, если у вас мало памяти и вы неожиданно тестируете на >= TRIM_MEMORY, потому что Android вызовет LOW MEMORY вызов, и это выше, чем у вас.Так что будьте осторожны, как / что вы тестируете.

Кроме того, некоторые люди спрашивают, как определить, когда вы вернетесь.

Простейший способ, которым я могу придумать, объяснен ниже, но, поскольку некоторые люди не знакомы с ним, я добавляю здесь псевдокод.Предполагая, что у вас есть YourApplication и MemoryBoss классы, в вашем class BaseActivity extends Activity (вам нужно будет создать один, если у вас его нет).

@Override
protected void onStart() {
    super.onStart();

    if (mApplication.wasInBackground()) {
        // HERE YOU CALL THE CODE YOU WANT TO HAPPEN ONLY ONCE WHEN YOUR APP WAS RESUMED FROM BACKGROUND
        mApplication.setWasInBackground(false);
    }
}

Я рекомендую onStart, потому что диалоги могут приостанавливатьсятак что держу пари, что вы не хотите, чтобы ваше приложение считало «оно ушло в фон», если все, что вы делали, это отображали полноэкранное диалоговое окно, но ваш пробег может отличаться.

И это все.Код в блоке if будет выполняться только один раз , даже если вы перейдете к другому действию, новое (которое также extends BaseActivity) сообщит, что wasInBackground равно false, поэтому не будетвыполнить код, , пока не будет вызван onMemoryTrimmed и флаг снова не будет установлен в true .

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

ОБНОВЛЕНИЕ / ЗАМЕТКИ (апрель 2015 г.) : Перед тем, как начать копирование и вставку этого кода, обратите внимание, что я обнаружил пару случаев, когда он может быть ненадежным на 100% и необходимо комбинировать с другими методами для достижения наилучших результатов.Примечательно, что есть два известных случая , в которых обратный вызов onTrimMemory не гарантированно будет выполнен:

  1. Если ваш телефон блокирует экран, когда ваше приложение видно(скажем, ваше устройство блокируется через nn минут), этот обратный вызов не вызывается (или не всегда), потому что экран блокировки находится только сверху, но ваше приложение все еще «работает», хотя и закрыто.

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

СейчасВ зависимости от того, насколько важно для вас знать, когда ваше приложение перешло в фоновый режим, вам может понадобиться, а может и не понадобиться расширять это решение вместе с отслеживанием жизненного цикла действия и так далее.

Просто имейте это в виду и создайте хорошую команду для проверки качества;)

КОНЕЦ ОБНОВЛЕНИЯ

Возможно, уже поздно, но есть надежный методв Ice Cream Sandwich (API 14) и выше .

Оказывается, что когда в вашем приложении больше нет видимого пользовательского интерфейса, вызывается обратный вызов.Обратный вызов, который вы можете реализовать в пользовательском классе, называется ComponentCallbacks2 (да, с двумя).Этот обратный вызов доступен только в API Level 14 (Ice Cream Sandwich) и выше.

Вы в основном получаете вызов метода:

public abstract void onTrimMemory (int level)

Уровень20 или более конкретно

public static final int TRIM_MEMORY_UI_HIDDEN

Я тестировал это, и оно всегда работает, потому что уровень 20 - это просто "предложение", что вы можете захотеть освободить некоторые ресурсы, так как ваше приложение больше не видно.

Чтобы процитировать официальные документы:

Уровень onTrimMemory (int): процесс показывал пользовательский интерфейс и больше не делает этого.Большие выделения с пользовательским интерфейсом должны быть освобождены в этот момент, чтобы обеспечить лучшее управление памятью.

Конечно, вы должны реализовать это, чтобы фактически делать то, что он говорит (очистить память, которая не использоваласьв определенное время очистите некоторые коллекции, которые оставались неиспользованными и т. д. Возможности безграничны (другие официальные документы могут найти более критические уровни).

Но, что интересно,это то, что ОС говорит вам: ЭЙ, ваше приложение ушло в фоновый режим!

Это именно то, что вы хотели узнать в первую очередь.

Как вы определяете, когда вы вернетесь?

Ну, это легко, я уверен, что у вас есть "BaseActivity", поэтому вы можете использовать onResume (), чтобы отметить тот факт, что вы вернулись.будет говорить, что вы не вернулись, когда вы фактически получите вызов к вышеуказанному методу onTrimMemory.

Это работает. Вы не получаете ложных срабатываний. Если действие возобновляется, вы вернулись100% случаев.Если пользователь снова переходит на задний план, вы получаете еще один onTrimMemory() звонок.

Вам необходимо указать свои действия (или, еще лучше, пользовательский класс).

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

public class MemoryBoss implements ComponentCallbacks2 {
    @Override
    public void onConfigurationChanged(final Configuration newConfig) {
    }

    @Override
    public void onLowMemory() {
    }

    @Override
    public void onTrimMemory(final int level) {
        if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // We're in the Background
        }
        // you might as well implement some memory cleanup here and be a nice Android dev.
    }
}

Чтобы использовать это, в вашей реализации приложения ( у вас есть один, RIGHT? ), сделайте что-нибудьнапример:

MemoryBoss mMemoryBoss;
@Override
public void onCreate() {
   super.onCreate();
   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
      mMemoryBoss = new MemoryBoss();
      registerComponentCallbacks(mMemoryBoss);
   } 
}

Если вы создадите Interface, вы можете добавить else к этому if и реализовать ComponentCallbacks (без 2), используемый во всем, что ниже API 14. Этот только обратный вызовимеет метод onLowMemory() и не вызывается при переходе на фон , но вы должны использовать его для обрезки памяти.

Теперь запустите приложение и нажмите home.Ваш onTrimMemory(final int level) метод должен быть вызван (подсказка: добавить ведение журнала).

Последний шаг - отменить регистрацию в обратном вызове.Вероятно, лучшим местом является метод onTerminate() вашего приложения, , но , этот метод не вызывается на реальном устройстве:

/**
 * This method is for use in emulated process environments.  It will
 * never be called on a production Android device, where processes are
 * removed by simply killing them; no user code (including this callback)
 * is executed when doing so.
 */

Такесли у вас действительно нет ситуации, когда вы больше не хотите регистрироваться, вы можете игнорировать ее, поскольку ваш процесс все равно умирает на уровне ОС.

Если вы решите отменить регистрацию в какой-то момент (если вы, дляНапример, предоставьте механизм выключения для вашего приложения, чтобы очистить и умереть), вы можете сделать:

unregisterComponentCallbacks(mMemoryBoss);

И это все.

174 голосов
/ 22 марта 2013

Вот как мне удалось это решить.Он работает исходя из того, что использование временной привязки между переходами активности, скорее всего, предоставит достаточные доказательства того, что приложение «фоновое» или нет.

Во-первых, я использовал экземпляр android.app.Application (давайте назовем это MyApplication), у которого есть Timer, TimerTask, константа, представляющая максимальное количество миллисекунд, которые разумно может занять переход от одного действия к другому (я пошел со значением 2 с), и логическое значение, чтобы указать,не приложение было "в фоновом режиме":

public class MyApplication extends Application {

    private Timer mActivityTransitionTimer;
    private TimerTask mActivityTransitionTimerTask;
    public boolean wasInBackground;
    private final long MAX_ACTIVITY_TRANSITION_TIME_MS = 2000;
    ...

Приложение также предоставляет два метода для запуска и остановки таймера / задачи:

public void startActivityTransitionTimer() {
    this.mActivityTransitionTimer = new Timer();
    this.mActivityTransitionTimerTask = new TimerTask() {
        public void run() {
            MyApplication.this.wasInBackground = true;
        }
    };

    this.mActivityTransitionTimer.schedule(mActivityTransitionTimerTask,
                                           MAX_ACTIVITY_TRANSITION_TIME_MS);
}

public void stopActivityTransitionTimer() {
    if (this.mActivityTransitionTimerTask != null) {
        this.mActivityTransitionTimerTask.cancel();
    }

    if (this.mActivityTransitionTimer != null) {
        this.mActivityTransitionTimer.cancel();
    }

    this.wasInBackground = false;
}

Последняя часть этого решениядобавить вызов к каждому из этих методов из событий onResume () и onPause () всех действий или, предпочтительно, в базовом действии, от которого наследуются все ваши конкретные действия:

@Override
public void onResume()
{
    super.onResume();

    MyApplication myApp = (MyApplication)this.getApplication();
    if (myApp.wasInBackground)
    {
        //Do specific came-here-from-background code
    }

    myApp.stopActivityTransitionTimer();
}

@Override
public void onPause()
{
    super.onPause();
    ((MyApplication)this.getApplication()).startActivityTransitionTimer();
}

Итак, вслучай, когда пользователь просто перемещается между действиями вашего приложения, onPause ()rting активность запускает таймер, но почти сразу новая введенная активность отменяет таймер, прежде чем он достигнет максимального времени перехода.И поэтому wasInBackground будет false .

С другой стороны, когда активность выходит на передний план из панели запуска, активируется устройство, завершается телефонный звонок и т. Д.Более вероятно, что задание таймера было выполнено до этого события, и поэтому wasInBackground было установлено на true .

131 голосов
/ 08 марта 2017

Редактировать: новые компоненты архитектуры принесли многообещающее: ProcessLifecycleOwner , см. @ vokilam's answer


Фактическое решение согласно Google I / O talk :

class YourApplication : Application() {

  override fun onCreate() {
    super.onCreate()
    registerActivityLifecycleCallbacks(AppLifecycleTracker())
  }

}


class AppLifecycleTracker : Application.ActivityLifecycleCallbacks  {

  private var numStarted = 0

  override fun onActivityStarted(activity: Activity?) {
    if (numStarted == 0) {
      // app went to foreground
    }
    numStarted++
  }

  override fun onActivityStopped(activity: Activity?) {
    numStarted--
    if (numStarted == 0) {
      // app went to background
    }
  }

}

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

Но есть надежда.

90 голосов
/ 09 июня 2017

ProcessLifecycleOwner также представляется многообещающим решением.

ProcessLifecycleOwner будет отправлять события ON_START, ON_RESUME, когда первое действие проходит через эти события. ON_PAUSE, ON_STOP, события будут отправляться с задержкой после того, как через них прошло последнее действие. Эта задержка достаточно велика, чтобы гарантировать, что ProcessLifecycleOwner не отправит никаких событий, если действия будут уничтожены и воссозданы из-за изменения конфигурации.

Реализация может быть такой простой, как

public class AppLifecycleListener implements LifecycleObserver {

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    public void onMoveToForeground() {
        // app moved to foreground
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    public void onMoveToBackground() {
       // app moved to background
    }
}

// register observer
ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifecycleListener());

Согласно исходному коду, текущее значение задержки составляет 700ms.

Также для использования этой функции требуется dependencies:

implementation "android.arch.lifecycle:extensions:1.1.1" 
annotationProcessor "android.arch.lifecycle:compiler:1.1.1"
88 голосов
/ 11 декабря 2010

Методы onPause() и onResume() вызываются, когда приложение переводится в фоновый режим и снова на передний план.Однако они также вызываются при первом запуске приложения и до его закрытия.Вы можете прочитать больше в Активность .

Нет прямого подхода к получению статуса приложения в фоновом режиме или на переднем плане, но даже я сталкивался с этимпроблема и найдены решения с onWindowFocusChanged и onStop.

Для получения более подробной информации проверьте здесь Android: решение для определения, когда приложение Android переходит в фоновый режим и возвращается на передний планбез getRunningTasks или getRunningAppProcesses .

65 голосов
/ 02 июня 2015

На основании ответа Мартина Марконциниса (спасибо!) Я наконец нашел надежное (и очень простое) решение.

public class ApplicationLifecycleHandler implements Application.ActivityLifecycleCallbacks, ComponentCallbacks2 {

    private static final String TAG = ApplicationLifecycleHandler.class.getSimpleName();
    private static boolean isInBackground = false;

    @Override
    public void onActivityCreated(Activity activity, Bundle bundle) {
    }

    @Override
    public void onActivityStarted(Activity activity) {
    }

    @Override
    public void onActivityResumed(Activity activity) {

        if(isInBackground){
            Log.d(TAG, "app went to foreground");
            isInBackground = false;
        }
    }

    @Override
    public void onActivityPaused(Activity activity) {
    }

    @Override
    public void onActivityStopped(Activity activity) {
    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
    }

    @Override
    public void onActivityDestroyed(Activity activity) {
    }

    @Override
    public void onConfigurationChanged(Configuration configuration) {
    }

    @Override
    public void onLowMemory() {
    }

    @Override
    public void onTrimMemory(int i) {
        if(i == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN){
            Log.d(TAG, "app went to background");
            isInBackground = true;
        }
    }
}

Затем добавьте это в свой onCreate () класса приложения

public class MyApp extends android.app.Application {

    @Override
    public void onCreate() {
        super.onCreate();

        ApplicationLifeCycleHandler handler = new ApplicationLifeCycleHandler();
        registerActivityLifecycleCallbacks(handler);
        registerComponentCallbacks(handler);

    }

}
59 голосов
/ 30 августа 2014

Мы используем этот метод.Это выглядит слишком просто для работы, но оно было хорошо протестировано в нашем приложении и фактически работает на удивление хорошо во всех случаях, включая переход на домашний экран с помощью кнопки «домой», с помощью кнопки «возврат» или после блокировки экрана.Попробуйте.

Идея состоит в том, что на переднем плане Android всегда запускает новую активность непосредственно перед остановкой предыдущей.Это не гарантировано, но вот как это работает.Кстати, Flurry, похоже, использует ту же логику (просто предположение, я этого не проверял, но перехватывает те же события).

public abstract class BaseActivity extends Activity {

    private static int sessionDepth = 0;

    @Override
    protected void onStart() {
        super.onStart();       
        sessionDepth++;
        if(sessionDepth == 1){
        //app came to foreground;
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (sessionDepth > 0)
            sessionDepth--;
        if (sessionDepth == 0) {
            // app went to background
        }
    }

}

Редактировать: согласно комментариям мы также перешли к onStart () в более поздних версиях кода.Кроме того, я добавляю супер звонки, которые отсутствовали в моем первоначальном посте, потому что это была скорее концепция, чем рабочий код.

55 голосов
/ 26 марта 2012

Если ваше приложение состоит из нескольких активностей и / или сложенных активностей, таких как виджет панели вкладок, то переопределение onPause () и onResume () не будет работать. Т.е. при запуске нового действия текущие действия будут приостановлены перед созданием нового. То же самое относится и к завершению (с помощью кнопки «назад») действия.

Я нашел два метода, которые, кажется, работают как хотелось.

Первый требует разрешения GET_TASKS и состоит из простого метода, который проверяет, относится ли наиболее активное действие на устройстве к приложению, путем сравнения имен пакетов:

private boolean isApplicationBroughtToBackground() {
    ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    List<RunningTaskInfo> tasks = am.getRunningTasks(1);
    if (!tasks.isEmpty()) {
        ComponentName topActivity = tasks.get(0).topActivity;
        if (!topActivity.getPackageName().equals(context.getPackageName())) {
            return true;
        }
    }

    return false;
}

Этот метод был найден в структуре Droid-Fu (теперь называется Ignition).

Второй метод, который я реализовал самостоятельно, не требует разрешения GET_TASKS, что хорошо. Вместо этого его немного сложнее реализовать.

В вашем классе MainApplication у вас есть переменная, которая отслеживает количество запущенных действий в вашем приложении. В onResume () для каждого действия вы увеличиваете переменную, а в onPause () вы уменьшаете ее.

Когда количество запущенных операций достигает 0, приложение переводится в фоновый режим, если выполняются следующие условия:

  • Приостановка действия не завершена (использовалась кнопка «назад»). Это можно сделать с помощью метода activity.isFinishing ()
  • Новое действие (с тем же именем пакета) не запускается. Вы можете переопределить метод startActivity (), чтобы установить переменную, которая указывает на это, а затем сбросить его в onPostResume (), который является последним методом, запускаемым при создании / возобновлении действия.

Когда вы можете определить, что приложение перешло в фоновый режим, его легко обнаружить, когда оно вернется на передний план.

33 голосов
/ 03 августа 2015

Создайте класс , который расширяет Application.Тогда в нем мы можем использовать метод переопределения onTrimMemory().

. Чтобы определить, переместилось ли приложение в фоновый режим, мы будем использовать:

 @Override
    public void onTrimMemory(final int level) {
        if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { // Works for Activity
            // Get called every-time when application went to background.
        } 
        else if (level == ComponentCallbacks2.TRIM_MEMORY_COMPLETE) { // Works for FragmentActivty
        }
    }
18 голосов
/ 21 сентября 2012

Рассмотрите возможность использования onUserLeaveHint.Это будет вызываться только тогда, когда ваше приложение переходит в фоновый режим.onPause будет иметь дело с угловыми случаями, так как он может быть вызван по другим причинам;например, если пользователь открывает другое действие в вашем приложении, например страницу настроек, будет вызываться метод onPause вашего основного действия, даже если они все еще находятся в вашем приложении;отслеживание происходящего приведет к ошибкам, когда вместо этого вы можете просто использовать обратный вызов onUserLeaveHint, который делает то, что вы запрашиваете.

Когда вызывается метод UserLeaveHint, вы можете установить для логического флага inBackground значение true.Когда вызывается onResume, только предполагайте, что вы вернулись на передний план, если установлен флаг inBackground.Это связано с тем, что onResume также будет вызываться для вашей основной активности, если пользователь был только в вашем меню настроек и никогда не выходил из приложения.

Помните, что если пользователь нажимает кнопку «Домой» во время отображения экрана настроек, onUserLeaveHint будетвызываться в настройках активности, а когда они возвращаются, onResume вызывается в настройках активности.Если у вас есть только этот код обнаружения в вашей основной деятельности, вы пропустите этот вариант использования.Чтобы этот код был во всех ваших действиях без дублирования кода, создайте абстрактный класс действий, расширяющий Activity, и вставьте в него свой общий код.Тогда каждое ваше занятие может расширять это абстрактное занятие.

Например:

public abstract AbstractActivity extends Activity {
    private static boolean inBackground = false;

    @Override
    public void onResume() {
        if (inBackground) {
            // You just came from the background
            inBackground = false;
        }
        else {
            // You just returned from another activity within your own app
        }
    }

    @Override
    public void onUserLeaveHint() {
        inBackground = true;
    }
}

public abstract MainActivity extends AbstractActivity {
    ...
}

public abstract SettingsActivity extends AbstractActivity {
    ...
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...