Поддержка обновлений внутри приложения (Kotlin или Java)

В этом руководстве описывается, как реализовать поддержку внутренних обновлений в вашем приложении, используя Kotlin или Java. Существуют отдельные руководства для случаев, когда ваша реализация использует нативный код (C/C++), и для случаев, когда ваша реализация использует Unity или Unreal Engine .

Настройте среду разработки

Библиотека обновления Play In-App входит в состав основных библиотек Google Play . Для интеграции библиотеки обновления Play In-App включите следующую зависимость Gradle.

Круто

// In your app's build.gradle file:
...
dependencies {
    // This dependency is downloaded from the Google's Maven repository.
    // So, make sure you also include that repository in your project's build.gradle file.
    implementation 'com.google.android.play:app-update:2.1.0'

    // For Kotlin users also add the Kotlin extensions library for Play In-App Update:
    implementation 'com.google.android.play:app-update-ktx:2.1.0'
    ...
}

Котлин

// In your app's build.gradle.kts file:
...
dependencies {
    // This dependency is downloaded from the Google's Maven repository.
    // So, make sure you also include that repository in your project's build.gradle file.
    implementation("com.google.android.play:app-update:2.1.0")

    // For Kotlin users also import the Kotlin extensions library for Play In-App Update:
    implementation("com.google.android.play:app-update-ktx:2.1.0")
    ...
}

Проверить наличие обновлений

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

Котлин

val appUpdateManager = AppUpdateManagerFactory.create(context)

// Returns an intent object that you use to check for an update.
val appUpdateInfoTask = appUpdateManager.appUpdateInfo

// Checks that the platform will allow the specified type of update.
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
    if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
        // This example applies an immediate update. To apply a flexible update
        // instead, pass in AppUpdateType.FLEXIBLE
        && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
    ) {
        // Request the update.
    }
}

Ява

AppUpdateManager appUpdateManager = AppUpdateManagerFactory.create(context);

// Returns an intent object that you use to check for an update.
Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo();

// Checks that the platform will allow the specified type of update.
appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> {
    if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
          // This example applies an immediate update. To apply a flexible update
          // instead, pass in AppUpdateType.FLEXIBLE
          && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
              // Request the update.
    }
});

Возвращаемый экземпляр AppUpdateInfo содержит статус доступности обновления. В зависимости от статуса обновления, экземпляр также содержит следующие данные:

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

Проверить актуальность обновлений

Помимо проверки наличия обновления, вы также можете проверить, сколько времени прошло с момента последнего уведомления пользователя о нём через Play Маркет. Это поможет вам решить, следует ли инициировать гибкое или немедленное обновление. Например, вы можете подождать несколько дней, прежде чем уведомить пользователя о гибком обновлении, и ещё несколько дней после этого, прежде чем потребовать немедленное обновление.

Используйте clientVersionStalenessDays() для проверки количества дней с момента, когда обновление стало доступно в Play Store:

Котлин

val appUpdateManager = AppUpdateManagerFactory.create(context)

// Returns an intent object that you use to check for an update.
val appUpdateInfoTask = appUpdateManager.appUpdateInfo

// Checks whether the platform allows the specified type of update,
// and current version staleness.
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
    if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
          && (appUpdateInfo.clientVersionStalenessDays() ?: -1) >= DAYS_FOR_FLEXIBLE_UPDATE
          && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
              // Request the update.
    }
}

Ява

AppUpdateManager appUpdateManager = AppUpdateManagerFactory.create(context);

// Returns an intent object that you use to check for an update.
Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo();

// Checks whether the platform allows the specified type of update,
// and current version staleness.
appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> {
    if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
          && appUpdateInfo.clientVersionStalenessDays() != null
          && appUpdateInfo.clientVersionStalenessDays() >= DAYS_FOR_FLEXIBLE_UPDATE
          && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
              // Request the update.
    }
});

Проверить приоритет обновления

API разработчика Google Play позволяет вам устанавливать приоритет каждого обновления. Это позволяет вашему приложению самостоятельно определять, насколько настоятельно рекомендовать обновление пользователю. Например, рассмотрим следующую стратегию установки приоритета обновления:

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

Для определения приоритета Google Play использует целое число от 0 до 5, где 0 — значение по умолчанию, а 5 — наивысший приоритет. Чтобы задать приоритет обновления, используйте поле inAppUpdatePriority в разделе Edits.tracks.releases в API разработчика Google Play. Все новые версии в релизе считаются имеющими тот же приоритет, что и сам релиз. Приоритет можно задать только при выпуске нового релиза и нельзя изменить позже.

Установите приоритет с помощью API Google Play Developer, как описано в документации API Play Developer . Приоритет обновления внутри приложения должен быть указан в ресурсе Edit.tracks , передаваемом в методе Edit.tracks: update :. В следующем примере демонстрируется выпуск приложения с кодом версии 88 и inAppUpdatePriority 5:

{
  "releases": [{
      "versionCodes": ["88"],
      "inAppUpdatePriority": 5,
      "status": "completed"
  }]
}

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

  • Вы выпускаете версию 1 в производство без приоритета.
  • Вы выпускаете версию 2 для внутреннего тестирования с приоритетом 5.
  • Вы выпускаете версию 3 в производство без приоритета.

Когда пользователи производственной среды обновляются с версии 1 до версии 3, они получают приоритет 5, даже если версия 2 была опубликована по другой схеме.

Котлин

val appUpdateManager = AppUpdateManagerFactory.create(context)

// Returns an intent object that you use to check for an update.
val appUpdateInfoTask = appUpdateManager.appUpdateInfo

// Checks whether the platform allows the specified type of update,
// and checks the update priority.
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
    if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
          && appUpdateInfo.updatePriority() >= 4 /* high priority */
          && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
              // Request an immediate update.
    }
}

Ява

AppUpdateManager appUpdateManager = AppUpdateManagerFactory.create(context);

// Returns an intent object that you use to check for an update.
Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo();

// Checks whether the platform allows the specified type of update,
// and checks the update priority.
appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> {
    if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
          && appUpdateInfo.updatePriority() >= 4 /* high priority */
          && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
              // Request an immediate update.
    }
});

Начать обновление

После подтверждения доступности обновления вы можете запросить обновление с помощью AppUpdateManager.startUpdateFlowForResult() :

Котлин

appUpdateManager.startUpdateFlowForResult(
    // Pass the intent that is returned by 'getAppUpdateInfo()'.
    appUpdateInfo,
    // an activity result launcher registered via registerForActivityResult
    activityResultLauncher,
    // Or pass 'AppUpdateType.FLEXIBLE' to newBuilder() for
    // flexible updates.
    AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build())

Ява

appUpdateManager.startUpdateFlowForResult(
    // Pass the intent that is returned by 'getAppUpdateInfo()'.
    appUpdateInfo,
    // an activity result launcher registered via registerForActivityResult
    activityResultLauncher,
    // Or pass 'AppUpdateType.FLEXIBLE' to newBuilder() for
    // flexible updates.
    AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build());

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

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

Дальнейшие действия зависят от того, запрашиваете ли вы гибкое обновление или немедленное обновление .

Настройте обновление с помощью AppUpdateOptions

AppUpdateOptions содержит поле AllowAssetPackDeletion , которое определяет, разрешено ли обновлению удалять пакеты ресурсов при ограниченном объёме памяти устройства. По умолчанию это поле имеет значение false , но вы можете использовать метод setAllowAssetPackDeletion() , чтобы установить его в true :

Котлин

appUpdateManager.startUpdateFlowForResult(
    // Pass the intent that is returned by 'getAppUpdateInfo()'.
    appUpdateInfo,
    // an activity result launcher registered via registerForActivityResult
    activityResultLauncher,
    // Or pass 'AppUpdateType.FLEXIBLE' to newBuilder() for
    // flexible updates.
    AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE)
        .setAllowAssetPackDeletion(true)
        .build())

Ява

appUpdateManager.startUpdateFlowForResult(
    // Pass the intent that is returned by 'getAppUpdateInfo()'.
    appUpdateInfo,
    // an activity result launcher registered via registerForActivityResult
    activityResultLauncher,
    // Or pass 'AppUpdateType.FLEXIBLE' to newBuilder() for
    // flexible updates.
    AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE)
        .setAllowAssetPackDeletion(true)
        .build());

Получите обратный звонок для обновления статуса

После запуска обновления обратный вызов запуска зарегистрированного результата активности получает результат диалогового окна подтверждения:

Котлин

registerForActivityResult(StartIntentSenderForResult()) { result: ActivityResult ->
    // handle callback
    if (result.resultCode != RESULT_OK) {
        log("Update flow failed! Result code: " + result.resultCode);
        // If the update is canceled or fails,
        // you can request to start the update again.
    }
}

Ява

registerForActivityResult(
    new ActivityResultContracts.StartIntentSenderForResult(),
    new ActivityResultCallback<ActivityResult>() {
        @Override
        public void onActivityResult(ActivityResult result) {
            // handle callback
            if (result.getResultCode() != RESULT_OK) {
                log("Update flow failed! Result code: " + result.getResultCode());
                // If the update is canceled or fails,
                // you can request to start the update again.
            }
        }
    });

Существует несколько значений, которые вы можете получить от обратного вызова onActivityResult() :

  • RESULT_OK : Пользователь принял обновление. В случае немедленных обновлений этот обратный вызов может не быть получен, поскольку обновление должно быть уже завершено к моменту возврата управления приложению.
  • RESULT_CANCELED : Пользователь отклонил или отменил обновление.
  • ActivityResult.RESULT_IN_APP_UPDATE_FAILED : Какая-то другая ошибка помешала пользователю предоставить согласие или продолжить обновление.

Обеспечить гибкое обновление

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

Мониторинг состояния гибкого обновления

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

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

Котлин

// Create a listener to track request state updates.
val listener = InstallStateUpdatedListener { state ->
    // (Optional) Provide a download progress bar.
    if (state.installStatus() == InstallStatus.DOWNLOADING) {
      val bytesDownloaded = state.bytesDownloaded()
      val totalBytesToDownload = state.totalBytesToDownload()
      // Show update progress bar.
    }
    // Log state or install the update.
}

// Before starting an update, register a listener for updates.
appUpdateManager.registerListener(listener)

// Start an update.

// When status updates are no longer needed, unregister the listener.
appUpdateManager.unregisterListener(listener)

Ява

// Create a listener to track request state updates.
InstallStateUpdatedListener listener = state -> {
  // (Optional) Provide a download progress bar.
  if (state.installStatus() == InstallStatus.DOWNLOADING) {
      long bytesDownloaded = state.bytesDownloaded();
      long totalBytesToDownload = state.totalBytesToDownload();
      // Implement progress bar.
  }
  // Log state or install the update.
};

// Before starting an update, register a listener for updates.
appUpdateManager.registerListener(listener);

// Start an update.

// When status updates are no longer needed, unregister the listener.
appUpdateManager.unregisterListener(listener);

Установить гибкое обновление

При обнаружении состояния InstallStatus.DOWNLOADED необходимо перезапустить приложение, чтобы установить обновление.

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

Рекомендуется предоставить уведомление (или иной индикатор пользовательского интерфейса), информирующее пользователя о готовности обновления к установке и запрашивающее подтверждение перед перезапуском приложения.

В следующем примере демонстрируется реализация снэк-бара Material Design , который запрашивает у пользователя подтверждение на перезапуск приложения:

Котлин

val listener = { state ->
    if (state.installStatus() == InstallStatus.DOWNLOADED) {
        // After the update is downloaded, show a notification
        // and request user confirmation to restart the app.
        popupSnackbarForCompleteUpdate()
    }
    ...
}

// Displays the snackbar notification and call to action.
fun popupSnackbarForCompleteUpdate() {
    Snackbar.make(
        findViewById(R.id.activity_main_layout),
        "An update has just been downloaded.",
        Snackbar.LENGTH_INDEFINITE
    ).apply {
        setAction("RESTART") { appUpdateManager.completeUpdate() }
        setActionTextColor(resources.getColor(R.color.snackbar_action_text_color))
        show()
    }
}

Ява

InstallStateUpdatedListener listener = state -> {
    if (state.installStatus() == InstallStatus.DOWNLOADED) {
        // After the update is downloaded, show a notification
        // and request user confirmation to restart the app.
        popupSnackbarForCompleteUpdate();
    }
    ...
};

// Displays the snackbar notification and call to action.
private void popupSnackbarForCompleteUpdate() {
  Snackbar snackbar =
      Snackbar.make(
          findViewById(R.id.activity_main_layout),
          "An update has just been downloaded.",
          Snackbar.LENGTH_INDEFINITE);
  snackbar.setAction("RESTART", view -> appUpdateManager.completeUpdate());
  snackbar.setActionTextColor(
      getResources().getColor(R.color.snackbar_action_text_color));
  snackbar.show();
}

При вызове appUpdateManager.completeUpdate() на переднем плане платформа отображает полноэкранный пользовательский интерфейс, который перезапускает приложение в фоновом режиме. После установки обновления платформа перезапускает приложение в свою основную активность.

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

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

Котлин

// Checks that the update is not stalled during 'onResume()'.
// However, you should execute this check at all app entry points.
override fun onResume() {
    super.onResume()

    appUpdateManager
        .appUpdateInfo
        .addOnSuccessListener { appUpdateInfo ->
            ...
            // If the update is downloaded but not installed,
            // notify the user to complete the update.
            if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
                popupSnackbarForCompleteUpdate()
            }
        }
}

Ява

// Checks that the update is not stalled during 'onResume()'.
// However, you should execute this check at all app entry points.
@Override
protected void onResume() {
  super.onResume();

  appUpdateManager
      .getAppUpdateInfo()
      .addOnSuccessListener(appUpdateInfo -> {
              ...
              // If the update is downloaded but not installed,
              // notify the user to complete the update.
              if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
                  popupSnackbarForCompleteUpdate();
              }
          });
}

Обрабатывать немедленное обновление

Когда вы запускаете немедленное обновление и пользователь соглашается на него, Google Play отображает ход обновления поверх пользовательского интерфейса вашего приложения на протяжении всего процесса. Если пользователь закроет или прекратит работу приложения во время обновления, загрузка и установка обновления продолжится в фоновом режиме без дополнительного подтверждения пользователя.

Однако, когда приложение возвращается на передний план, необходимо убедиться, что обновление не остановлено в состоянии UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS . Если обновление остановлено в этом состоянии, возобновите его:

Котлин

// Checks that the update is not stalled during 'onResume()'.
// However, you should execute this check at all entry points into the app.
override fun onResume() {
    super.onResume()

    appUpdateManager
        .appUpdateInfo
        .addOnSuccessListener { appUpdateInfo ->
            ...
            if (appUpdateInfo.updateAvailability()
                == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS
            ) {
                // If an in-app update is already running, resume the update.
                appUpdateManager.startUpdateFlowForResult(
                  appUpdateInfo,
                  activityResultLauncher,
                  AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build())
            }
        }
}

Ява

// Checks that the update is not stalled during 'onResume()'.
// However, you should execute this check at all entry points into the app.
@Override
protected void onResume() {
  super.onResume();

  appUpdateManager
      .getAppUpdateInfo()
      .addOnSuccessListener(
          appUpdateInfo -> {
            ...
            if (appUpdateInfo.updateAvailability()
                == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
                // If an in-app update is already running, resume the update.
                appUpdateManager.startUpdateFlowForResult(
                  appUpdateInfo,
                  activityResultLauncher,
                  AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build());
            }
          });
}

Поток обновления возвращает результат, описанный в справочной документации по методу startUpdateFlowForResult() . В частности, ваше приложение должно обрабатывать ситуации, когда пользователь отклоняет обновление или отменяет загрузку. При выполнении пользователем любого из этих действий пользовательский интерфейс Google Play закрывается. Ваше приложение должно определить оптимальный способ продолжения.

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

Следующие шаги

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