주문형 Google Play 서비스 모듈의 사용 가능 여부 관리

Google Play 서비스 개요 도움말에 설명된 대로 Google Play 서비스에서 제공하는 SDK는 Google 인증 Android 기기의 기기 내 서비스에 의해 지원됩니다. 전체 기기에서 스토리지와 메모리를 보존하기 위해 일부 서비스는 앱에 관련 기능이 필요할 때 주문형으로 설치되는 모듈로 제공됩니다. 예를 들어 Google Play 서비스에서 모델을 사용할 때 ML Kit가 이 옵션을 제공합니다.

대부분의 경우 앱에서 필요한 API를 사용하면 Google Play 서비스 SDK가 필요한 모듈을 자동으로 다운로드하여 설치합니다. 그러나 모듈을 미리 설치하여 사용자 환경을 개선하려는 경우와 같이 프로세스를 더 세부적으로 제어하고 싶을 수 있습니다.

ModuleInstallClient API를 사용하면 다음 작업을 할 수 있습니다.

  • 모듈이 이미 기기에 설치되어 있는지 확인합니다.
  • 모듈 설치를 요청합니다.
  • 설치 진행 상황을 모니터링합니다.
  • 설치 프로세스 중에 오류를 처리합니다.

이 가이드에서는 ModuleInstallClient를 사용하여 앱에서 모듈을 관리하는 방법을 보여줍니다. 다음 코드 스니펫에서는 TensorFlow Lite SDK(play-services-tflite-java)를 예로 사용하지만 이 단계는 OptionalModuleApi와 통합된 모든 라이브러리에 적용할 수 있습니다.

시작하기 전에

앱을 준비하려면 다음 섹션의 단계를 완료합니다.

앱 기본 요건

앱의 빌드 파일이 다음 값을 사용하는지 확인합니다.

  • minSdkVersion 23 이상

앱 구성

  1. 최상위 수준 settings.gradle 파일의 dependencyResolutionManagement 블록 내에 Google Maven 저장소Maven 중앙 저장소를 포함합니다.

    dependencyResolutionManagement {
        repositories {
            google()
            mavenCentral()
        }
    }
    
  2. 모듈의 Gradle 빌드 파일 (일반적으로 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)

    자바

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

    자바

    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)

    자바

    ModuleInstallClient moduleInstallClient = ModuleInstall.getClient(context);
  2. 지연된 요청을 전송합니다.

    Kotlin

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

    자바

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

긴급 모듈 설치 요청

앱에 모듈이 즉시 필요한 경우 긴급 설치를 요청할 수 있습니다. 이렇게 하면 모바일 데이터를 사용해야 하더라도 모듈을 최대한 빨리 설치하려고 시도합니다.

  1. ModuleInstallClient의 인스턴스를 가져옵니다.

    Kotlin

    val moduleInstallClient = ModuleInstall.getClient(context)

    자바

    ModuleInstallClient moduleInstallClient = ModuleInstall.getClient(context);
  2. (선택사항) InstallStatusListener를 만들어 설치 진행 상황을 모니터링합니다.

    앱의 UI에 다운로드 진행률을 표시하려면 (예: 진행률 표시줄 사용) 업데이트를 수신하는 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()

    자바

    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()

    자바

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

    자바

    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는 종속 항목 삽입을 사용하여 테스트에서 모듈 설치 API의 결과를 시뮬레이션할 수 있는 FakeModuleInstallClient를 제공합니다. 이렇게 하면 실제 기기에 배포하지 않고도 다양한 시나리오에서 앱의 동작을 테스트할 수 있습니다.

앱 기본 요건

Hilt 종속 항목 주입 프레임워크를 사용하도록 앱을 구성합니다.

테스트에서 ModuleInstallClientFakeModuleInstallClient로 바꿉니다.

테스트에서 FakeModuleInstallClient를 사용하려면 ModuleInstallClient 결합을 가짜 구현으로 대체해야 합니다.

  1. 종속 항목을 추가합니다.

    모듈의 Gradle 빌드 파일 (일반적으로 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. ModuleInstallClient를 제공하는 Hilt 모듈을 만듭니다.

    Kotlin

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

    자바

    @Module
    @InstallIn(ActivityComponent.class)
    public class ModuleInstallModule {
      @Provides
      public static ModuleInstallClient provideModuleInstallClient(
        @ActivityContext Context context) {
        return ModuleInstall.getClient(context);
      }
    }
  3. 활동에 ModuleInstallClient을(를) 삽입합니다.

    Kotlin

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

    자바

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

    자바

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

자바

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

자바

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