Обновление приложений и поддержка актуальной версии у пользователей это очень важный момент в жизненном цикле каждого приложения. От части это зависит от того, что Android постоянно изменяется: какие-то методы и классы добавляются, какие-то становятся устаревшими, и примерно раз в год с выходом новой версии следует обновлять targetSdkVersion и переписывать код, если это требуется. Кроме того, постоянно обновляются сторонние библиотеки, которые нужно также обновить у себя в приложении, добавляются новые функции и особенности в само приложение. И про багфиксы не стоит забывать, нужно постоянно отслеживать баги в приложениях и оперативно исправлять, потому что недовольный пользователь скорее всего испортит статистику приложению негативным отзывом.
Всё это вынуждает постоянно поддерживать приложение. Отсюда вытекает следующий момент: как установить актуальную версию приложения максимальному числу пользователей.
Этот вопрос от части решает сам Google Play. Он присылает уведомления пользователю о том, что доступна новая версия приложения, предлагает включить автоматическое обновление приложений, что обеспечивает самую свежую версию приложения на устройстве. Однако не все пользователи использую эту возможность, многие обновляют от случая к случаю, игнорируют уведомления.
В этом случае мы можем взять дело в свои руки и встроить механизм обновления в само приложения.
В прошлом году Google выпустили механизм In-App Updates с целью ещё больше ускорить обновление приложений у пользователей. In-App Updates позволяет показывать пользователю при входе в приложение диалог, где ему будет предложено обновить приложение до последней версии. Этот функционал работает, начиная с версии Android 5.0 (API 21). Кроме того, приложение должно быть опубликовано в Google Play.
In-App Updates поддерживает два способа обновления приложения:
- Flexible. Если пользователь согласится на обновление приложения, загрузка начнётся в фоновом режиме, при этом пользователь сможет продолжить пользоваться приложением. Как только загрузка завершится, в приложение (если оно активно) придёт результат загрузки, после чего мы можем либо предложить перезапустить приложение, либо дождаться, когда пользователь из него выйдет. Если же на момент окончания загрузки приложение находилось в фоновом режиме, то установка новой версии начнётся сразу же. Такой подход удобен в большинстве случаев, поскольку позволяет обновить приложение, не мешая пользователю.
- Immediate. Пользователь не сможет работать в приложении до тех пор, пока не обновится до новой версии. Когда он согласится на обновление, на этом же экране начнётся процесс загрузки и установки, после чего приложение перезапустится. Такой подход нужен в случае, если вышло критическое обновление приложения, либо если старая версия приложения более не может работать корректно. В остальных случаях лучше всего использовать первый вариант.
Рассмотрим первый вариант обновления приложения на примере одного из наших приложений “Страны мира“.
Перед началом работы нужно добавить библиотеку Play Core в приложение. Для этого в файле build.gradle модуля приложения добавим следующую зависимость:
implementation 'com.google.android.play:core:1.7.2'
Теперь создадим класс UpdateManager, в котором будем проводить работу, связанную с обновлением.
public final class UpdateManager { private static UpdateManager instance; private UpdateManager() { } public static UpdateManager getInstance() { if (instance == null) { instance = new UpdateManager(); } return instance; } }
Перед тем, как показывать диалог, следует проверить наличие новой версии. Для этого воспользуемся классом AppUpdateManager для запроса к Google Play.
private static UpdateManager instance; private AppUpdateManager appUpdateManager; ... public void checkForUpdate(Activity activity) { appUpdateManager = AppUpdateManagerFactory.create(activity); Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo(); appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> { if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) { } }); }
К экземпляру класса AppUpdateManager привязываем слушатель OnSuccessListener, который будет получать результат запроса. Если новая версия приложения доступна, отправляем интент, отображающий диалог для скачивания новой версии.
public final static int UPDATE_REQUEST_CODE = 8; ... public void checkForUpdate(Activity activity) { appUpdateManager = AppUpdateManagerFactory.create(activity); Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo(); appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> { if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) { if (!activity.isFinishing()) { try { startUpdate(appUpdateInfo, activity); } catch (IntentSender.SendIntentException e) { if (BuildConfig.DEBUG) { Log.e(ERROR_TAG, "SendError: " + e); } } } } }); } private void startUpdate(AppUpdateInfo appUpdateInfo, Activity activity) throws IntentSender.SendIntentException { appUpdateManager.startUpdateFlowForResult( appUpdateInfo, AppUpdateType.FLEXIBLE, activity, UPDATE_REQUEST_CODE); }
Если пользователь согласится на обновление, начнётся процесс загрузки в фоновом режиме. Обратите внимание, что с помощью метода startUpdateFlowForResult() мы можем узнать, какое действие выбрал пользователь. Для этого мы передаём в него константу UPDATE_REQUEST_CODE, результат с этим кодом придёт в метод onActivityResult() активности.
@Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (requestCode == UpdateManager.UPDATE_REQUEST_CODE) { if (resultCode != RESULT_OK) { Log.d(LOG_TAG, "error"); } } super.onActivityResult(requestCode, resultCode, data); }
Здесь мы можем получить один из следующих результатов:
- RESULT_OK – пользователь согласился на обновление. Если вместо Flexible используется Immediate, то результат может не прийти, поскольку в этот момент Google Play возьмёт работу на себя.
- RESULT_CANCELED – пользователь отклонил или отменил обновление.
- ActivityResult.RESULT_IN_APP_UPDATE_FAILED – произошла какая-то другая ошибка, из-за которой пользователь не смог дать разрешение или отклонить обновление.
Поскольку мы используем Flexible метод обновления, нам нужно узнать, когда завершится загрузка новой версии. Для этого нам нужно добавить слушатель InstallStateUpdatedListener и привязать его к экземпляру AppUpdateManager после того, как начнётся загрузка. О том, что пользователь начал загрузку, мы узнаём, получим результат в методе onActivityResult() активности.
public void registerListener() { if (appUpdateManager == null) return; appUpdateManager.registerListener(listener); } private final InstallStateUpdatedListener listener = state -> { if (state.installStatus() == InstallStatus.DOWNLOADED) { } };
С помощью этого слушателя мы можем узнать, загружается обновление или уже загружено, и если загружается – получить прогресс выполнения. Таким образом, при желании можно показывать прогресс загрузки прямо в приложении, но в данном случае мы этого делать не будем.
Когда загрузка завершена, отвязываем слушатель и проверяем, активно приложение или нет. Если приложение активно, то показываем сообщение о том, что обновление готово для установки, в противном случае запускаем установку обновления. Делается это с помощью метода completeUpdate() экземпляра AppUpdateManager. Также для этих целей мы добавим слушатель UpdateListener к нашему классу UpdateManager. Если этот слушатель привязан к активности – значит приложение активно, если отвязан – значит находится в фоне.
public final class UpdateManager { public interface UpdateListener { void onShowSnackbar(); } private UpdateListener updateListener; ... private final InstallStateUpdatedListener listener = state -> { if (state.installStatus() == InstallStatus.DOWNLOADED) { onUpdateDownloaded(); } }; private void onUpdateDownloaded() { if (appUpdateManager == null) return; appUpdateManager.unregisterListener(listener); if (updateListener != null) { updateListener.onShowSnackbar(); } else { appUpdateManager.completeUpdate(); } } }
Когда обновление будет загружено, пользователь увидит внизу экрана Snackbar с сообщением об этом и кнопкой для перезагрузки приложения. При желании можно Snackbar заменить на что-нибудь ещё.
Сам же Snackbar выглядит очень просто, при нажатии на Restart мы вызываем completeUpdate(), как делали бы это, будучи в фоновом режиме.
Snackbar snackbar = Snackbar.make(mainView.getRootView(), "An update has just been downloaded.", 10000); snackbar.setAction("RESTART", view -> UpdateManager.getInstance().completeUpdate()); snackbar.setActionTextColor( ContextCompat.getColor(mainView.onGetContext(), R.color.colorAccent)); snackbar.show();
Если пользователь проигнорирует это сообщение, обновление останется неустановленным и будет храниться в памяти. Поэтому при каждом запуске приложения мы будем проверять, имеются ли загруженные обновления, и если да – выводить сообщение. В противном случае, как и раньше, проверяем наличие обновлений в Google Play. Для этого вернёмся к слушателю OnSuccessListener из начала и добавим в него следующее условие.
public void checkForUpdate(Activity activity) { appUpdateManager = AppUpdateManagerFactory.create(activity); Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo(); appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> { if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) { if (updateListener != null) { updateListener.onShowSnackbar(); } } else if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) { if (!activity.isFinishing()) { try { startUpdate(appUpdateInfo, activity); } catch (IntentSender.SendIntentException e) { if (BuildConfig.DEBUG) { Log.e(ERROR_TAG, "SendError: " + e); } } } } }); }
Таким образом сообщение будет выводиться до тех пор, пока пользователь не обновит приложение до последней версии.
Итоговый листинг класса UpdateManager выглядит следующий образом:
public final class UpdateManager { public interface UpdateListener { void onShowSnackbar(); } private UpdateListener updateListener; private static UpdateManager instance; private AppUpdateManager appUpdateManager; public final static int UPDATE_REQUEST_CODE = 8; private final String ERROR_TAG = "UPDATE_ERROR"; private UpdateManager() { } public static UpdateManager getInstance() { if (instance == null) { instance = new UpdateManager(); } return instance; } public void attachUpdateListener(UpdateListener listener) { updateListener = listener; } public void detachUpdateListener() { updateListener = null; } public void checkForUpdate(Activity activity) { appUpdateManager = AppUpdateManagerFactory.create(activity); Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo(); appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> { if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) { if (updateListener != null) { updateListener.onShowSnackbar(); } } else if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) { if (!activity.isFinishing()) { try { startUpdate(appUpdateInfo, activity); } catch (IntentSender.SendIntentException e) { if (BuildConfig.DEBUG) { Log.e(ERROR_TAG, "SendError: " + e); } } } } }); } private void startUpdate(AppUpdateInfo appUpdateInfo, Activity activity) throws IntentSender.SendIntentException { if (appUpdateManager == null) return; appUpdateManager.startUpdateFlowForResult( appUpdateInfo, AppUpdateType.FLEXIBLE, activity, UPDATE_REQUEST_CODE); } public void registerListener() { if (appUpdateManager == null) return; appUpdateManager.registerListener(listener); } public void completeUpdate() { if (appUpdateManager == null) return; appUpdateManager.completeUpdate(); } private final InstallStateUpdatedListener listener = state -> { if (state.installStatus() == InstallStatus.DOWNLOADED) { onUpdateDownloaded(); } }; private void onUpdateDownloaded() { if (appUpdateManager == null) return; appUpdateManager.unregisterListener(listener); if (updateListener != null) { updateListener.onShowSnackbar(); } else { appUpdateManager.completeUpdate(); } } }
Итак, мы встроили In-App Updates в приложение, осталось только проверить его работу. Для этого отлично подойдёт такая особенность Google Play, как внутренний совместный доступ (Internal app-sharing). Он позволяет загружать APK или Android App Bundle на специальную страницу загрузки, к которым будут иметь доступ только тестировщики. Это позволяет быстро проверить приложение, при этом его не обязательно подписывать релизным ключом, можно делиться также и debug-версиями. Тестировщикам отправляется специальная ссылка на скачивание, которая перенаправляет их в Google Play, где им будет предложено установить данную версию приложения.
Для того, чтобы настроить внутренний совместный доступ, нам нужно задать список тестировщиков для этого приложения. Зайдём в консоль разработчика Google Play и откроем нужное нам приложение. В меню “Инструменты разработки” выберем “Внутренний доступ к приложению“, после чего откроется страница для настройки. Здесь нас интересует только “Управление пользователями с правами загрузки“.
Тут мы можем формировать различные списки тестировщиков, включать их и отключать. При нажатии на “Создать список” появится окно, где будет предложено ввести электронную почту каждого из участников тестирования, а также задать имя новому списку. Создадим список и добавим в него себя как тестировщика.
Теперь мы можем перейти на страницу загрузки для внутреннего совместного доступа. Сюда мы может публиковать различные APK для тестирования. Соберём два APK нашего приложения с разными кодами версий (например, 23 и 24) и загрузим их на эту страницу. При загрузке мы также можем задать название данной версии, чтобы было проще их различать.
После того, как APK будут загружены, они отобразятся в списке.
С этого момента мы готовы тестировать эти версии. Зайдём на устройство, с которого будем запускать приложение, и на нём перейдём по ссылке от версии 1.23. Откроется специальная страница, которая предложит перейти в Google Play для установки.
После перехода в Google Play нам будет предложено установить эту версию приложения.
Примечание! Если вместо страницы приложения вы видите сообщение “Настройки для разработчиков отключены”, то нужно будет выполнить следующее:
- Зайдите на устройстве в Google Play.
- Откройте Настройки и найдите внизу “Версия Play Маркета”.
- Кликайте по этой надписи несколько раз, до тех пор пока не появится уведомление “Вы стали разработчиком“. После этого у вас появятся дополнительные настройки в Google Play.
- Включите появившуюся опцию сверху “Внутренний доступ к приложениям“.
- Теперь при переходе по ссылке у вас будет загружаться страница приложения.
Как только установка завершится, открываем приложение. В нашем приложении под списком меню указывается текущий код и версия приложения, поэтому будем ориентироваться на неё, что обновление прошло успешно. Как видно, сейчас текущая версия 1.23.
Теперь закрываем приложение, снова открываем браузер и переходим уже по ссылке от версии 1.24. Нам аналогично будет предложено перейти в Google Play, только на этот раз мы можем не установить, а обновить или удалить приложение. Важно: не нужно обновлять приложение с этой страницы! Смотрим только, что она есть, и закрываем.
Снова заходим в наше приложение и, если всё сделано верно, отобразится диалог с предложением обновить версию.
Нажимаем на “Обновить“, после чего начинается скачивание обновления. За его прогрессом можно следить в шторке уведомлений.
Как только загрузка завершится, в наш слушатель придёт состояние DOWNLOADED, после чего внизу экрана отобразится сообщение с предложением перезагрузить приложение.
Нажимаем Restart, и здесь в работу включаются сервисы Google Play, которые обновят приложение до новой версии.
После того, как установка завершится, приложение запустится автоматически. Скроллим список вниз и видим, что версия приложения теперь стала 1.24, значит обновление прошло успешно.
Таким образом, благодаря In-App Updates, пользователи смогут более быстро получать новые версии, чем при обновлении непосредственно через Google Play.
А как сослаться на UpdateManager например в MainActivity?
А как сослаться на UpdateManager например в MainActivity? Что нужно писать в MainActivity что бы вызвать проверку и установку?
Здравствуйте!
UpdateManager это singleton-класс, поэтому вы можете обращаться к нему через метод UpdateManager.getInstance(), который вернёт экземпляр класса, и затем уже вызывать нужные методы.
Например, чтобы вызвать проверку установки, нужно написать следующее:
UpdateManager.getInstance().checkForUpdate(this);
Делать это можно, например, в onCreate() активности.
Также в методах onResume() и onPause() следует привязывать и отвязывать соответственно слушатель.
@Override public void onResume() {
UpdateManager.getInstance().attachUpdateListener(updateListener);
}
@Override public void onPause() {
UpdateManager.getInstance().detachUpdateListener();
}
Когда обновление загружено и готово к установке, запустить его можно следующим образом:
UpdateManager.getInstance().completeUpdate()