<br />

As described in the [Overview of Google Play services](https://developers.google.com/android/guides/overview.md.txt)
article, SDKs powered by Google Play services are backed by on-device services
on Google-certified Android devices. To conserve storage and memory across the
entire fleet of devices, some services are provided as modules that are
installed on-demand when your app requires the relevant functionality. For
example, [ML Kit provides this option](https://developers.google.com/ml-kit/vision/barcode-scanning/android.md.txt#before-you-begin)
when using models in Google Play services.

In most cases, the Google Play services SDK downloads and installs the necessary
modules automatically when your app uses an API that requires them.
However, you might want to have more control over the process, such as when you
want to improve the user experience by installing the module in advance.

The `ModuleInstallClient` API gives you the ability to:

- Check if the modules are already installed on the device.
- Request to install the modules.
- Monitor the installation progress.
- Handle errors during the installation process.

This guide shows you how to use `ModuleInstallClient` to manage modules in your
app. Note that the following code snippets use the
[TensorFlow Lite SDK](https://www.tensorflow.org/lite/android/play_services)
(`play-services-tflite-java`) as an example, but these steps are applicable for
any library that's integrated with `OptionalModuleApi`.

## Before you begin

To prepare your app, complete the steps in the following sections.

### App prerequisites

Make sure that your app's build file uses the following values:

- A `minSdkVersion` of `23` or higher

### Configure your app

1. In your top-level `settings.gradle` file, include
   [Google's Maven repository](https://maven.google.com/web/index.html) and the
   [Maven central repository](https://search.maven.org/artifact) within the
   `dependencyResolutionManagement` block:

   ```groovy
   dependencyResolutionManagement {
       repositories {
           google()
           mavenCentral()
       }
   }
   ```
2. In your module's Gradle build file (usually `app/build.gradle`), add the
   Google Play services dependencies for `play-services-base` and
   `play-services-tflite-java`:

   ```groovy
   dependencies {
     implementation 'com.google.android.gms:play-services-base:18.11.1'
     implementation 'com.google.android.gms:play-services-tflite-java:16.5.0'
   }
   ```

## Check if modules are available

Before you try to install a module, you can check if it's already installed on
the device. This helps you avoid unnecessary installation requests.

1. Get an instance of `ModuleInstallClient`:

   ### Kotlin

   ```kotlin
   val moduleInstallClient = ModuleInstall.getClient(context)
   ```

   ### Java

   ```java
   ModuleInstallClient moduleInstallClient = ModuleInstall.getClient(context);
   ```
2. Check the availability of a module using its `OptionalModuleApi`.
   This API is provided by the Google Play services SDK that you are using.

   ### Kotlin

   ```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

   ```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...
           });
   ```

## Request a deferred install

If you don't need the module immediately, you can request a deferred install.
This allows Google Play services to install the module in the background,
potentially when the device is idle and connected to Wi-Fi.

1. Get an instance of `ModuleInstallClient`:

   ### Kotlin

   ```kotlin
   val moduleInstallClient = ModuleInstall.getClient(context)
   ```

   ### Java

   ```java
   ModuleInstallClient moduleInstallClient = ModuleInstall.getClient(context);
   ```
2. Send the deferred request:

   ### Kotlin

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

   ### Java

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

## Request an urgent module install

If your app needs the module immediately, you can request an urgent install.
This will try to install the module as quickly as possible, even if it means
using mobile data.

1. Get an instance of `ModuleInstallClient`:

   ### Kotlin

   ```kotlin
   val moduleInstallClient = ModuleInstall.getClient(context)
   ```

   ### Java

   ```java
   ModuleInstallClient moduleInstallClient = ModuleInstall.getClient(context);
   ```
2. (Optional) Create an `InstallStatusListener` to monitor the install progress.

   If you want to display the download progress in your app's UI (for example,
   with a progress bar), you can create an `InstallStatusListener` to receive
   updates.

   ### Kotlin

   ```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

   ```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. Configure the `ModuleInstallRequest` and add the `OptionalModuleApi` to the
   request:

   ### Kotlin

   ```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

   ```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. Send the install request:

   ### Kotlin

   ```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

   ```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...
           });
   ```

## Test your app with `FakeModuleInstallClient`

Google Play services SDKs provide the `FakeModuleInstallClient` to allow you to
simulate the results of the module install APIs in tests using dependency
injection. This helps you to test your app's behavior in different scenarios
without needing to deploy it to a real device.

### App prerequisites

Configure your app to use
[Hilt dependency injection framework](https://developer.android.com/training/dependency-injection/hilt-android).

### Replace `ModuleInstallClient` with `FakeModuleInstallClient` in test

To use `FakeModuleInstallClient` in your tests, you need to replace the
`ModuleInstallClient` binding with the fake implementation.

1. Add dependency:

   In your module's Gradle build file (usually `app/build.gradle`), add the
   Google Play services dependencies for `play-services-base-testing` in your
   test.

   ```groovy
     dependencies {
       // other dependencies...

       testImplementation 'com.google.android.gms:play-services-base-testing:16.2.0'
     }
   ```
2. Create a Hilt module to provide `ModuleInstallClient`:

   ### Kotlin

   ```kotlin
   @Module
   @InstallIn(ActivityComponent::class)
   object ModuleInstallModule {

     @Provides
     fun provideModuleInstallClient(
       @ActivityContext context: Context
     ): ModuleInstallClient = ModuleInstall.getClient(context)
   }
   ```

   ### Java

   ```java
   @Module
   @InstallIn(ActivityComponent.class)
   public class ModuleInstallModule {
     @Provides
     public static ModuleInstallClient provideModuleInstallClient(
       @ActivityContext Context context) {
       return ModuleInstall.getClient(context);
     }
   }
   ```
3. Inject the `ModuleInstallClient` in the activity:

   ### Kotlin

   ```kotlin
   @AndroidEntryPoint
   class MyActivity: AppCompatActivity() {
     @Inject lateinit var moduleInstallClient: ModuleInstallClient

     ...
   }
   ```

   ### Java

   ```java
   @AndroidEntryPoint
   public class MyActivity extends AppCompatActivity {
     @Inject ModuleInstallClient moduleInstallClient;

     ...
   }
   ```
4. Replace the binding in test:

   ### Kotlin

   ```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

   ```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;

     ...
   }
   ```

   > [!NOTE]
   > **Note:** This snippet shows a basic example of using `@BindValue` to bind a `ModuleInstallClient` field to `FakeModuleInstallClient` in your test into the Hilt dependency graph. You can also follow the [Hilt testing guide](https://developer.android.com/training/dependency-injection/hilt-testing) to create your own test module and replace the binding there.

### Simulate different scenarios

With `FakeModuleInstallClient`, you can simulate different scenarios, such as:

- Modules are already installed.
- Modules are not available on the device.
- The installation process fails.
- The deferred install request is successful or fails.
- The urgent install request is successful or fails.

### Kotlin

```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

```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...
}
```

### Simulate result for a deferred install request

### Kotlin

```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

```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...
}
```

### Simulate result for an urgent install request

### Kotlin

```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

```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...
}
```