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

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

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

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

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

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

시작하기 전에

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

앱 기본 요건

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

  • minSdkVersion23 이상

앱 구성

  1. 최상위 수준 settings.gradle 파일의 dependencyResolutionManagement 블록 내에 Google's Maven repositoryMaven central repository를 포함합니다.

    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.10.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를 만들어 설치 진행 상황을 모니터링합니다.

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

    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는 종속 항목 삽입을 사용하여 테스트에서 모듈 설치 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.2.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. 활동에 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...
}