管理按需 Google Play 服务模块的可用性

Google Play 服务概览一文所述,由 Google Play 服务提供支持的 SDK 由 Google 认证的 Android 设备上的设备端服务提供支持。为了节省整个设备阵容中的存储空间和内存,部分服务以模块的形式提供,这些模块会在您的应用需要相关功能时按需安装。例如,在 Google Play 服务中使用模型时,机器学习套件会提供此选项

在大多数情况下,当您的应用使用需要这些模块的 API 时,Google Play 服务 SDK 会自动下载并安装必要的模块。不过,您可能希望更好地控制该过程,例如,您希望通过提前安装模块来改善用户体验。

借助 ModuleInstallClient API,您可以:

  • 检查模块是否已安装到设备上。
  • 请求安装模块。
  • 监控安装进度。
  • 处理安装过程中的错误。

本指南介绍了如何使用 ModuleInstallClient 管理应用中的模块。请注意,以下代码段使用 TensorFlow Lite SDK (play-services-tflite-java) 作为示例,但这些步骤适用于与 OptionalModuleApi 集成的任何库。

准备工作

为了让您的应用做好准备,请完成以下部分中的步骤。

应用要满足的前提条件

确保您应用的 build 文件使用以下值:

  • minSdkVersion23 或更高版本

配置您的应用

  1. 在顶级 settings.gradle 文件的 dependencyResolutionManagement 代码块中,添加 Google 的 Maven 制品库Maven 中央制品库

    dependencyResolutionManagement {
        repositories {
            google()
            mavenCentral()
        }
    }
    
  2. 在模块的 Gradle build 文件(通常为 app/build.gradle)中,添加 play-services-baseplay-services-tflite-java 的 Google Play 服务依赖项:

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

检查模块是否可用

在尝试安装模块之前,您可以检查该模块是否已安装在设备上。这有助于避免不必要的安装请求。

  1. 获取 ModuleInstallClient 的实例:

    Kotlin

    val moduleInstallClient = ModuleInstall.getClient(context)

    Java

    ModuleInstallClient moduleInstallClient = ModuleInstall.getClient(context);
  2. 使用模块的 OptionalModuleApi 检查模块的可用性。此 API 由您使用的 Google Play 服务 SDK 提供。

    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…
            });

请求延迟安装

如果您不需要立即安装该模块,可以请求延迟安装。这样,Google Play 服务便可以在后台安装模块,可能在设备处于空闲状态且连接到 Wi-Fi 时执行此操作。

  1. 获取 ModuleInstallClient 的实例:

    Kotlin

    val moduleInstallClient = ModuleInstall.getClient(context)

    Java

    ModuleInstallClient moduleInstallClient = ModuleInstall.getClient(context);
  2. 发送延迟请求:

    Kotlin

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

    Java

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

请求紧急安装模块

如果您的应用需要立即使用该模块,可以申请紧急安装。这将尝试尽快安装模块,即使这意味着使用移动流量也是如此。

  1. 获取 ModuleInstallClient 的实例:

    Kotlin

    val moduleInstallClient = ModuleInstall.getClient(context)

    Java

    ModuleInstallClient moduleInstallClient = ModuleInstall.getClient(context);
  2. (可选)创建 InstallStatusListener 以监控安装进度。

    如果您想在应用的界面中显示下载进度(例如,使用进度条),可以创建 InstallStatusListener 来接收更新。

    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. 配置 ModuleInstallRequest 并将 OptionalModuleApi 添加到请求中:

    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. 发送安装请求:

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

使用 FakeModuleInstallClient 测试应用

Google Play 服务 SDK 提供了 FakeModuleInstallClient,以便您使用依赖项注入在测试中模拟模块安装 API 的结果。这有助于您在不同场景中测试应用的行为,而无需将其部署到真实设备。

应用要满足的前提条件

将应用配置为使用 Hilt 依赖项注入框架

在测试中将 ModuleInstallClient 替换为 FakeModuleInstallClient

如需在测试中使用 FakeModuleInstallClient,您需要将 ModuleInstallClient 绑定替换为虚构实现。

  1. 添加依赖项:

    在模块的 Gradle build 文件(通常为 app/build.gradle)中,在测试中添加 play-services-base-testing 的 Google Play 服务依赖项。

      dependencies {
        // other dependencies...
    
        testImplementation 'com.google.android.gms:play-services-base-testing:16.1.0'
      }
    
  2. 创建一个 Hilt 模块来提供 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. 在 activity 中注入 ModuleInstallClient

    Kotlin

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

    Java

    @AndroidEntryPoint
    public class MyActivity extends AppCompatActivity {
      @Inject ModuleInstallClient moduleInstallClient;
    
      ...
    }
  4. 替换测试中的绑定:

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

模拟不同的场景

借助 FakeModuleInstallClient,您可以模拟不同的场景,例如:

  • 模块已安装。
  • 设备上不支持模块。
  • 安装过程失败。
  • 延迟安装请求是成功还是失败。
  • 紧急安装请求是成功还是失败。

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

模拟延迟安装请求的结果

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

模拟紧急安装请求的结果

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