Встраиваем In-App Updates в приложение

Автор: | 23.04.2020

Обновление приложений и поддержка актуальной версии у пользователей это очень важный момент в жизненном цикле каждого приложения. От части это зависит от того, что Android постоянно изменяется: какие-то методы и классы добавляются, какие-то становятся устаревшими, и примерно раз в год с выходом новой версии следует обновлять targetSdkVersion и переписывать код, если это требуется. Кроме того, постоянно обновляются сторонние библиотеки, которые нужно также обновить у себя в приложении, добавляются новые функции и особенности в само приложение. И про багфиксы не стоит забывать, нужно постоянно отслеживать баги в приложениях и оперативно исправлять, потому что недовольный пользователь скорее всего испортит статистику приложению негативным отзывом.

Всё это вынуждает постоянно поддерживать приложение. Отсюда вытекает следующий момент: как установить актуальную версию приложения максимальному числу пользователей.

Этот вопрос от части решает сам Google Play. Он присылает уведомления пользователю о том, что доступна новая версия приложения, предлагает включить автоматическое обновление приложений, что обеспечивает самую свежую версию приложения на устройстве. Однако не все пользователи использую эту возможность, многие обновляют от случая к случаю, игнорируют уведомления.

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

В прошлом году Google выпустили механизм In-App Updates с целью ещё больше ускорить обновление приложений у пользователей. In-App Updates позволяет показывать пользователю при входе в приложение диалог, где ему будет предложено обновить приложение до последней версии. Этот функционал работает, начиная с версии Android 5.0 (API 21). Кроме того, приложение должно быть опубликовано в Google Play.

In-App Updates поддерживает два способа обновления приложения:

  1. Flexible. Если пользователь согласится на обновление приложения, загрузка начнётся в фоновом режиме, при этом пользователь сможет продолжить пользоваться приложением. Как только загрузка завершится, в приложение (если оно активно) придёт результат загрузки, после чего мы можем либо предложить перезапустить приложение, либо дождаться, когда пользователь из него выйдет. Если же на момент окончания загрузки приложение находилось в фоновом режиме, то установка новой версии начнётся сразу же. Такой подход удобен в большинстве случаев, поскольку позволяет обновить приложение, не мешая пользователю.
  2. 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 нам будет предложено установить эту версию приложения.

Примечание! Если вместо страницы приложения вы видите сообщение «Настройки для разработчиков отключены», то нужно будет выполнить следующее:

  1. Зайдите на устройстве в Google Play.
  2. Откройте Настройки и найдите внизу «Версия Play Маркета».
  3. Кликайте по этой надписи несколько раз, до тех пор пока не появится уведомление «Вы стали разработчиком«. После этого у вас появятся дополнительные настройки в Google Play.
  4. Включите появившуюся опцию сверху «Внутренний доступ к приложениям«.
  5. Теперь при переходе по ссылке у вас будет загружаться страница приложения.

Как только установка завершится, открываем приложение. В нашем приложении под списком меню указывается текущий код и версия приложения, поэтому будем ориентироваться на неё, что обновление прошло успешно. Как видно, сейчас текущая версия 1.23.

Теперь закрываем приложение, снова открываем браузер и переходим уже по ссылке от версии 1.24. Нам аналогично будет предложено перейти в Google Play, только на этот раз мы можем не установить, а обновить или удалить приложение. Важно: не нужно обновлять приложение с этой страницы! Смотрим только, что она есть, и закрываем.

Снова заходим в наше приложение и, если всё сделано верно, отобразится диалог с предложением обновить версию.

Нажимаем на «Обновить«, после чего начинается скачивание обновления. За его прогрессом можно следить в шторке уведомлений.

Как только загрузка завершится, в наш слушатель придёт состояние DOWNLOADED, после чего внизу экрана отобразится сообщение с предложением перезагрузить приложение.

Нажимаем Restart, и здесь в работу включаются сервисы Google Play, которые обновят приложение до новой версии.

После того, как установка завершится, приложение запустится автоматически. Скроллим список вниз и видим, что версия приложения теперь стала 1.24, значит обновление прошло успешно.

Таким образом, благодаря In-App Updates, пользователи смогут более быстро получать новые версии, чем при обновлении непосредственно через Google Play.

Встраиваем In-App Updates в приложение: 3 комментария

  1. Алексей

    А как сослаться на UpdateManager например в MainActivity?

  2. Алексей

    А как сослаться на UpdateManager например в MainActivity? Что нужно писать в MainActivity что бы вызвать проверку и установку?

    1. Владимир

      Здравствуйте!
      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()

Добавить комментарий для Владимир Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *