Administra la disponibilidad de los módulos de Servicios de Google Play on demand

Como se describe en el artículo Descripción general de los Servicios de Google Play, los SDKs con la tecnología de los Servicios de Google Play cuentan con la compatibilidad de los servicios integrados en el dispositivo en dispositivos Android certificados por Google. Para conservar el almacenamiento y la memoria en toda la flota de dispositivos, algunos servicios se proporcionan como módulos que se instalan a pedido cuando tu app requiere la funcionalidad relevante. Por ejemplo, ML Kit proporciona esta opción cuando se usan modelos en los Servicios de Google Play.

En la mayoría de los casos, el SDK de Servicios de Google Play descarga e instala los módulos necesarios automáticamente cuando tu app usa una API que los requiere. Sin embargo, es posible que desees tener más control sobre el proceso, por ejemplo, cuando desees mejorar la experiencia del usuario instalando el módulo con anticipación.

La API de ModuleInstallClient te permite hacer lo siguiente:

  • Comprueba si los módulos ya están instalados en el dispositivo.
  • Solicita la instalación de los módulos.
  • Supervisa el progreso de la instalación.
  • Controla los errores durante el proceso de instalación.

En esta guía, se muestra cómo usar ModuleInstallClient para administrar módulos en tu app. Ten en cuenta que los siguientes fragmentos de código usan el SDK de TensorFlow Lite (play-services-tflite-java) como ejemplo, pero estos pasos se aplican a cualquier biblioteca que esté integrada con OptionalModuleApi.

Antes de comenzar

Para preparar tu app, completa los pasos que se indican en las siguientes secciones.

Requisitos previos de la app

Asegúrate de que el archivo de compilación de tu app use los siguientes valores:

  • Una minSdkVersion de 23 o más

Cómo configurar tu app

  1. En el archivo settings.gradle de nivel superior, incluye el repositorio de Maven de Google y el repositorio central de Maven dentro del bloque dependencyResolutionManagement:

    dependencyResolutionManagement {
        repositories {
            google()
            mavenCentral()
        }
    }
    
  2. En el archivo de compilación de Gradle de tu módulo (generalmente app/build.gradle), agrega las dependencias de los Servicios de Google Play para play-services-base y play-services-tflite-java:

    dependencies {
      implementation 'com.google.android.gms:play-services-base:18.7.0'
      implementation 'com.google.android.gms:play-services-tflite-java:16.4.0'
    }
    

Cómo comprobar si hay módulos disponibles

Antes de intentar instalar un módulo, puedes verificar si ya está instalado en el dispositivo. Esto te ayuda a evitar solicitudes de instalación innecesarias.

  1. Obtén una instancia de ModuleInstallClient:

    Kotlin

    val moduleInstallClient = ModuleInstall.getClient(context)

    Java

    ModuleInstallClient moduleInstallClient = ModuleInstall.getClient(context);
  2. Verifica la disponibilidad de un módulo con su OptionalModuleApi. El SDK de Servicios de Google Play que usas proporciona esta API.

    Kotlin

    val optionalModuleApi = TfLite.getClient(context)
    moduleInstallClient
      .areModulesAvailable(optionalModuleApi)
      .addOnSuccessListener {
        if (it.areModulesAvailable()) {
          // Modules are present on the device...
        } else {
          // Modules are not present on the device...
        }
      }
      .addOnFailureListener {
        // Handle failure...
      }

    Java

    OptionalModuleApi optionalModuleApi = TfLite.getClient(context);
    moduleInstallClient
        .areModulesAvailable(optionalModuleApi)
        .addOnSuccessListener(
            response -> {
              if (response.areModulesAvailable()) {
                // Modules are present on the device...
              } else {
                // Modules are not present on the device...
              }
            })
        .addOnFailureListener(
            e -> {
              // Handle failure…
            });

Cómo solicitar una instalación diferida

Si no necesitas el módulo de inmediato, puedes solicitar una instalación diferida. Esto permite que los Servicios de Google Play instalen el módulo en segundo plano, posiblemente cuando el dispositivo esté inactivo y conectado a Wi-Fi.

  1. Obtén una instancia de ModuleInstallClient:

    Kotlin

    val moduleInstallClient = ModuleInstall.getClient(context)

    Java

    ModuleInstallClient moduleInstallClient = ModuleInstall.getClient(context);
  2. Envía la solicitud diferida:

    Kotlin

    val optionalModuleApi = TfLite.getClient(context)
    moduleInstallClient.deferredInstall(optionalModuleApi)

    Java

    OptionalModuleApi optionalModuleApi = TfLite.getClient(context);
    moduleInstallClient.deferredInstall(optionalModuleApi);

Cómo solicitar una instalación de módulo urgente

Si tu app necesita el módulo de inmediato, puedes solicitar una instalación urgente. Se intentará instalar el módulo lo más rápido posible, incluso si eso significa usar datos móviles.

  1. Obtén una instancia de ModuleInstallClient:

    Kotlin

    val moduleInstallClient = ModuleInstall.getClient(context)

    Java

    ModuleInstallClient moduleInstallClient = ModuleInstall.getClient(context);
  2. (Opcional) Crea un InstallStatusListener para supervisar el progreso de la instalación.

    Si quieres mostrar el progreso de la descarga en la IU de tu app (por ejemplo, con una barra de progreso), puedes crear un InstallStatusListener para recibir actualizaciones.

    Kotlin

    inner class ModuleInstallProgressListener : InstallStatusListener {
      override fun onInstallStatusUpdated(update: ModuleInstallStatusUpdate) {
        // Progress info is only set when modules are in the progress of downloading.
        update.progressInfo?.let {
          val progress = (it.bytesDownloaded * 100 / it.totalBytesToDownload).toInt()
          // Set the progress for the progress bar.
          progressBar.setProgress(progress)
        }
    
        if (isTerminateState(update.installState)) {
          moduleInstallClient.unregisterListener(this)
        }
      }
    
      fun isTerminateState(@InstallState state: Int): Boolean {
        return state == STATE_CANCELED || state == STATE_COMPLETED || state == STATE_FAILED
      }
    }
    
    val listener = ModuleInstallProgressListener()

    Java

    static final class ModuleInstallProgressListener implements InstallStatusListener {
        @Override
        public void onInstallStatusUpdated(ModuleInstallStatusUpdate update) {
          ProgressInfo progressInfo = update.getProgressInfo();
          // Progress info is only set when modules are in the progress of downloading.
          if (progressInfo != null) {
            int progress =
                (int)
                    (progressInfo.getBytesDownloaded() * 100 / progressInfo.getTotalBytesToDownload());
            // Set the progress for the progress bar.
            progressBar.setProgress(progress);
          }
          // Handle failure status maybe…
    
          // Unregister listener when there are no more install status updates.
          if (isTerminateState(update.getInstallState())) {
    
            moduleInstallClient.unregisterListener(this);
          }
        }
    
        public boolean isTerminateState(@InstallState int state) {
          return state == STATE_CANCELED || state == STATE_COMPLETED || state == STATE_FAILED;
        }
      }
    
    InstallStatusListener listener = new ModuleInstallProgressListener();
  3. Configura ModuleInstallRequest y agrega OptionalModuleApi a la solicitud:

    Kotlin

    val optionalModuleApi = TfLite.getClient(context)
    val moduleInstallRequest =
      ModuleInstallRequest.newBuilder()
        .addApi(optionalModuleApi)
        // Add more APIs if you would like to request multiple modules.
        // .addApi(...)
        // Set the listener if you need to monitor the download progress.
        // .setListener(listener)
        .build()

    Java

    OptionalModuleApi optionalModuleApi = TfLite.getClient(context);
    ModuleInstallRequest moduleInstallRequest =
        ModuleInstallRequest.newBuilder()
            .addApi(optionalModuleApi)
            // Add more API if you would like to request multiple modules
            //.addApi(...)
            // Set the listener if you need to monitor the download progress
            //.setListener(listener)
            .build();
  4. Envía la solicitud de instalación:

    Kotlin

    moduleInstallClient
      .installModules(moduleInstallRequest)
      .addOnSuccessListener {
        if (it.areModulesAlreadyInstalled()) {
          // Modules are already installed when the request is sent.
        }
        // The install request has been sent successfully. This does not mean
        // the installation is completed. To monitor the install status, set an
        // InstallStatusListener to the ModuleInstallRequest.
      }
      .addOnFailureListener {
        // Handle failure…
      }

    Java

    moduleInstallClient.installModules(moduleInstallRequest)
        .addOnSuccessListener(
            response -> {
              if (response.areModulesAlreadyInstalled()) {
                // Modules are already installed when the request is sent.
              }
              // The install request has been sent successfully. This does not
              // mean the installation is completed. To monitor the install
              // status, set an InstallStatusListener to the
              // ModuleInstallRequest.
            })
        .addOnFailureListener(
            e -> {
              // Handle failure...
            });

Prueba tu app con FakeModuleInstallClient

Los SDKs de Servicios de Google Play proporcionan FakeModuleInstallClient para permitirte simular los resultados de las APIs de instalación de módulos en pruebas con la inserción de dependencias. Esto te ayuda a probar el comportamiento de tu app en diferentes situaciones sin necesidad de implementarla en un dispositivo real.

Requisitos previos de la app

Configura tu app para usar el framework de inserción de dependencias de Hilt.

Reemplaza ModuleInstallClient por FakeModuleInstallClient en la prueba

Para usar FakeModuleInstallClient en tus pruebas, debes reemplazar la vinculación ModuleInstallClient con la implementación falsa.

  1. Agrega la dependencia:

    En el archivo de compilación de Gradle de tu módulo (generalmente app/build.gradle), agrega las dependencias de los Servicios de Google Play para play-services-base-testing en tu prueba.

      dependencies {
        // other dependencies...
    
        testImplementation 'com.google.android.gms:play-services-base-testing:16.1.0'
      }
    
  2. Crea un módulo de Hilt para proporcionar ModuleInstallClient:

    Kotlin

    @Module
    @InstallIn(ActivityComponent::class)
    object ModuleInstallModule {
    
      @Provides
      fun provideModuleInstallClient(
        @ActivityContext context: Context
      ): ModuleInstallClient = ModuleInstall.getClient(context)
    }

    Java

    @Module
    @InstallIn(ActivityComponent.class)
    public class ModuleInstallModule {
      @Provides
      public static ModuleInstallClient provideModuleInstallClient(
        @ActivityContext Context context) {
        return ModuleInstall.getClient(context);
      }
    }
  3. Inserta el ModuleInstallClient en la actividad:

    Kotlin

    @AndroidEntryPoint
    class MyActivity: AppCompatActivity() {
      @Inject lateinit var moduleInstallClient: ModuleInstallClient
    
      ...
    }

    Java

    @AndroidEntryPoint
    public class MyActivity extends AppCompatActivity {
      @Inject ModuleInstallClient moduleInstallClient;
    
      ...
    }
  4. Reemplaza la vinculación en la prueba:

    Kotlin

    @UninstallModules(ModuleInstallModule::class)
    @HiltAndroidTest
    class MyActivityTest {
      ...
      private val context:Context = ApplicationProvider.getApplicationContext()
      private val fakeModuleInstallClient = FakeModuleInstallClient(context)
      @BindValue @JvmField
      val moduleInstallClient: ModuleInstallClient = fakeModuleInstallClient
    
      ...
    }

    Java

    @UninstallModules(ModuleInstallModule.class)
    @HiltAndroidTest
    class MyActivityTest {
      ...
      private static final Context context = ApplicationProvider.getApplicationContext();
      private final FakeModuleInstallClient fakeModuleInstallClient = new FakeModuleInstallClient(context);
      @BindValue ModuleInstallClient moduleInstallClient = fakeModuleInstallClient;
    
      ...
    }

Simula diferentes situaciones

Con FakeModuleInstallClient, puedes simular diferentes situaciones, como las siguientes:

  • Los módulos ya están instalados.
  • Los módulos no están disponibles en el dispositivo.
  • Falla el proceso de instalación.
  • La solicitud de instalación diferida se realiza correctamente o falla.
  • La solicitud de instalación urgente se realiza correctamente o falla.

Kotlin

@Test
fun checkAvailability_available() {
  // Reset any previously installed modules.
  fakeModuleInstallClient.reset()

  val availableModule = TfLite.getClient(context)
  fakeModuleInstallClient.setInstalledModules(api)

  // Verify the case where modules are already available...
}

@Test
fun checkAvailability_unavailable() {
  // Reset any previously installed modules.
  fakeModuleInstallClient.reset()

  // Do not set any installed modules in the test.

  // Verify the case where modules unavailable on device...
}

@Test
fun checkAvailability_failed() {
  // Reset any previously installed modules.
  fakeModuleInstallClient.reset()

  fakeModuleInstallClient.setModulesAvailabilityTask(Tasks.forException(RuntimeException()))

  // Verify the case where an RuntimeException happened when trying to get module's availability...
}

Java

@Test
public void checkAvailability_available() {
  // Reset any previously installed modules.
  fakeModuleInstallClient.reset();

  OptionalModuleApi optionalModuleApi = TfLite.getClient(context);
  fakeModuleInstallClient.setInstalledModules(api);

  // Verify the case where modules are already available...
}

@Test
public void checkAvailability_unavailable() {
  // Reset any previously installed modules.
  fakeModuleInstallClient.reset();

  // Do not set any installed modules in the test.

  // Verify the case where modules unavailable on device...
}

@Test
public void checkAvailability_failed() {
  fakeModuleInstallClient.setModulesAvailabilityTask(Tasks.forException(new RuntimeException()));

  // Verify the case where an RuntimeException happened when trying to get module's availability...
}

Simula el resultado de una solicitud de instalación diferida

Kotlin

@Test
fun deferredInstall_success() {
  fakeModuleInstallClient.setDeferredInstallTask(Tasks.forResult(null))

  // Verify the case where the deferred install request has been sent successfully...
}

@Test
fun deferredInstall_failed() {
  fakeModuleInstallClient.setDeferredInstallTask(Tasks.forException(RuntimeException()))

  // Verify the case where an RuntimeException happened when trying to send the deferred install request...
}

Java

@Test
public void deferredInstall_success() {
  fakeModuleInstallClient.setDeferredInstallTask(Tasks.forResult(null));

  // Verify the case where the deferred install request has been sent successfully...
}

@Test
public void deferredInstall_failed() {
  fakeModuleInstallClient.setDeferredInstallTask(Tasks.forException(new RuntimeException()));

  // Verify the case where an RuntimeException happened when trying to send the deferred install request...
}

Simula el resultado de una solicitud de instalación urgente

Kotlin

@Test
fun installModules_alreadyExist() {
  // Reset any previously installed modules.
  fakeModuleInstallClient.reset();

  OptionalModuleApi optionalModuleApi = TfLite.getClient(context);
  fakeModuleInstallClient.setInstalledModules(api);

  // Verify the case where the modules already exist when sending the install request...
}

@Test
fun installModules_withoutListener() {
  // Reset any previously installed modules.
  fakeModuleInstallClient.reset();

  // Verify the case where the urgent install request has been sent successfully...
}

@Test
fun installModules_withListener() {
  // Reset any previously installed modules.
  fakeModuleInstallClient.reset();

  // Generates a ModuleInstallResponse and set it as the result for installModules().
  val moduleInstallResponse = FakeModuleInstallUtil.generateModuleInstallResponse()
  fakeModuleInstallClient.setInstallModulesTask(Tasks.forResult(moduleInstallResponse))

  // Verify the case where the urgent install request has been sent successfully...

  // Generates some fake ModuleInstallStatusUpdate and send it to listener.
  val update = FakeModuleInstallUtil.createModuleInstallStatusUpdate(
    moduleInstallResponse.sessionId, STATE_COMPLETED)
  fakeModuleInstallClient.sendInstallUpdates(listOf(update))

  // Verify the corresponding updates are handled correctly...
}

@Test
fun installModules_failed() {
  fakeModuleInstallClient.setInstallModulesTask(Tasks.forException(RuntimeException()))

  // Verify the case where an RuntimeException happened when trying to send the urgent install request...
}

Java

@Test
public void installModules_alreadyExist() {
  // Reset any previously installed modules.
  fakeModuleInstallClient.reset();

  OptionalModuleApi optionalModuleApi = TfLite.getClient(context);
  fakeModuleInstallClient.setInstalledModules(api);

  // Verify the case where the modules already exist when sending the install request...
}

@Test
public void installModules_withoutListener() {
  // Reset any previously installed modules.
  fakeModuleInstallClient.reset();

  // Verify the case where the urgent install request has been sent successfully...
}

@Test
public void installModules_withListener() {
  // Reset any previously installed modules.
  fakeModuleInstallClient.reset();

  // Generates a ModuleInstallResponse and set it as the result for installModules().
  ModuleInstallResponse moduleInstallResponse =
      FakeModuleInstallUtil.generateModuleInstallResponse();
  fakeModuleInstallClient.setInstallModulesTask(Tasks.forResult(moduleInstallResponse));

  // Verify the case where the urgent install request has been sent successfully...

  // Generates some fake ModuleInstallStatusUpdate and send it to listener.
  ModuleInstallStatusUpdate update = FakeModuleInstallUtil.createModuleInstallStatusUpdate(
      moduleInstallResponse.getSessionId(), STATE_COMPLETED);
  fakeModuleInstallClient.sendInstallUpdates(ImmutableList.of(update));

  // Verify the corresponding updates are handled correctly...
}

@Test
public void installModules_failed() {
  fakeModuleInstallClient.setInstallModulesTask(Tasks.forException(new RuntimeException()));

  // Verify the case where an RuntimeException happened when trying to send the urgent install request...
}