앱에 클라이언트 측 라이선스 확인 추가

경고: 앱이 클라이언트 측에서 라이선스 확인 프로세스를 실행하면 잠재적 공격자가 확인 프로세스와 관련된 로직을 더 쉽게 수정하거나 삭제할 수 있습니다.

이러한 이유로 서버 측 라이선스 확인을 실행하는 것을 적극 권장합니다.

게시자 계정과 개발 환경을 설정했다면(라이선스 설정 참조) 라이선스 확인 라이브러리(LVL)를 사용하여 앱에 라이선스 확인을 추가할 수 있습니다.

LVL을 사용하여 라이선스 확인을 추가하려면 다음 작업을 실행합니다.

  1. 애플리케이션의 매니페스트에 라이선스 권한 추가
  2. 정책 구현 - LVL에 제공된 전체 구현 중 하나를 선택하거나 자체적으로 만들 수 있습니다.
  3. Obfuscator 구현, Policy가 라이선스 응답 데이터를 캐시하는 경우
  4. 애플리케이션의 기본 활동에서 코드를 추가하여 라이선스 확인
  5. DeviceLimiter 구현(선택사항이며 대부분의 애플리케이션에 권장되지 않음)

아래 섹션에서는 이러한 작업을 설명합니다. 통합을 완료하면 애플리케이션을 성공적으로 컴파일할 수 있어야 하고 테스트 환경 설정에서 설명한 대로 테스트를 시작할 수 있습니다.

LVL에 포함된 전체 소스 파일 세트의 개요는 LVL 클래스 및 인터페이스 요약을 참조하세요.

라이선스 권한 추가

Google Play 애플리케이션을 사용하여 서버에 라이선스 확인을 전송하려면 애플리케이션에서 올바른 권한 com.android.vending.CHECK_LICENSE를 요청해야 합니다. 애플리케이션에서 라이선스 권한을 선언하지 않고 라이선스 확인을 시작하려는 경우 LVL에서 보안 예외가 발생합니다.

애플리케이션에서 라이선스 권한을 요청하려면 다음과 같이 <uses-permission> 요소를 <manifest>의 하위 요소로 선언합니다.

<uses-permission android:name="com.android.vending.CHECK_LICENSE" />

예를 들어 다음은 LVL 샘플 애플리케이션에서 권한을 선언하는 방법입니다.

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...">
    <!-- Devices >= 3 have version of Google Play that supports licensing. -->
    <uses-sdk android:minSdkVersion="3" />
    <!-- Required permission to check licensing. -->
    <uses-permission android:name="com.android.vending.CHECK_LICENSE" />
    ...
</manifest>

참고: 현재는 LVL 라이브러리 프로젝트의 매니페스트에서 CHECK_LICENSE 권한을 선언할 수 없습니다. SDK 도구가 종속 애플리케이션의 매니페스트에 이 권한을 병합하지 않기 때문입니다. 대신 각 종속 애플리케이션의 매니페스트에서 권한을 선언해야 합니다.

정책 구현

Google Play 라이선스 서비스는 특정 라이선스가 있는 특정 사용자에게 귀하의 애플리케이션 액세스 권한을 부여할지를 자체적으로 판단하지 않습니다. 오히려 그 책임은 애플리케이션에서 개발자가 제공하는 Policy 구현에 있습니다.

정책은 LVL에서 선언된 인터페이스입니다. LVL은 라이선스 확인 결과에 기반하여 사용자 액세스를 허용하거나 허용하지 않는 애플리케이션 로직을 보유하도록 설계되었습니다. LVL을 사용하려면 애플리케이션에서 Policy 구현을 제공해야 합니다.

Policy 인터페이스는 두 가지 메서드 allowAccess()processServerResponse()를 선언합니다. 이 메서드는 라이선스 서버의 응답을 처리할 때 LicenseChecker 인스턴스가 호출합니다. LicenseResponse라는 enum도 선언합니다. enum은 processServerResponse() 호출에 전달된 라이선스 응답 값을 지정합니다.

  • 액세스 권한을 부여할지 결정하기 전에 processServerResponse()를 사용하면 라이선스 서버에서 수신된 원시 응답 데이터를 사전 처리할 수 있습니다.

    일반적인 구현은 라이선스 응답에서 일부 또는 모든 필드를 추출하고 SharedPreferences 저장소를 통하는 등의 방식으로 영구 저장소에 데이터를 로컬로 저장하여 애플리케이션 호출 및 기기 전원을 껐다 켜는 동안 데이터에 액세스할 수 있도록 합니다. 예를 들어 Policy는 애플리케이션이 실행될 때마다 값을 재설정하기보다는 성공적인 마지막 라이선스 확인의 타임스탬프, 재시도 횟수, 라이선스 유효 기간, 영구 저장소의 유사 정보를 유지합니다.

    응답 데이터를 로컬에 저장하는 경우 Policy는 데이터가 난독화되어 있는지 확인해야 합니다(아래 Obfuscator 구현 참고).

  • allowAccess()는 라이선스 서버나 캐시에서 사용 가능한 라이선스 응답 데이터 또는 기타 애플리케이션별 정보에 기반하여 사용자에게 애플리케이션 액세스 권한을 부여할지 판단합니다. 예를 들어 allowAccess()의 구현에서는 백엔드 서버에서 검색된 사용량 또는 기타 데이터와 같은 추가 기준을 고려할 수 있습니다. 모든 경우에 allowAccess()의 구현은 라이선스 서버에서 판단한 대로 사용자가 라이선스를 부여받아 애플리케이션을 사용한다면 또는 일시적인 네트워크 또는 시스템 문제로 라이선스 확인을 완료할 수 없다면 true만 반환해야 합니다. 이러한 경우 구현에서는 재시도 응답 횟수를 유지하여 다음 라이선스 확인이 완료될 때까지 잠정적으로 액세스를 허용할 수 있습니다.

애플리케이션에 라이선스를 추가하는 프로세스를 간소화하고 Policy를 어떻게 디자인해야 하는지 보여주기 위해 LVL에는 수정하지 않고 사용하거나 필요에 맞게 조정할 수 있는 두 개의 전체 Policy 구현이 있습니다.

  • ServerManagedPolicy, 유연한 Policy로 서버 제공 설정 및 캐시된 응답을 사용하여 다양한 네트워크 조건에서 액세스를 관리
  • StrictPolicy, 응답 데이터를 캐시하지 않으며 서버에서 라이선스가 부여된 응답을 반환할 때 액세스를 허용

대부분의 애플리케이션에는 ServerManagedPolicy를 사용하는 것이 더 좋습니다. ServerManagedPolicy는 LVL 기본값이며 LVL 샘플 애플리케이션과 통합되어 있습니다.

맞춤 정책 가이드라인

라이선스 구현에서 LVL(ServerManagedPolicy 또는 StrictPolicy)에 제공된 전체 정책 중 하나를 사용하거나 맞춤 정책을 만들 수 있습니다. 모든 맞춤 정책 유형에는 구현에서 이해하고 고려할 여러 중요한 디자인 요소가 있습니다.

라이선스 서버는 일반적인 요청 한도를 적용하여 서비스 거부를 초래할 수 있는 리소스 남용을 방지합니다. 애플리케이션이 요청 한도를 초과하면 라이선스 서버가 503 응답을 반환하고 일반적인 서버 오류로 애플리케이션에 전달됩니다. 즉, 한도가 재설정될 때까지 어떤 라이선스 응답도 사용자에게 제공되지 않으므로 사용자에게 무기한 영향을 미칠 수 있습니다.

맞춤 정책을 설계하는 경우 Policy가 다음과 같이 하는 것이 좋습니다.

  1. 가장 최근 성공한 라이선스 응답을 로컬 영구 저장소에 캐시 및 적절히 난독화
  2. 라이선스 서버에 요청하기보다는 캐시된 응답이 유효하다면 모든 라이선스 확인의 캐시된 응답을 반환. 서버 제공 VT Extras에 따라 응답 유효성을 설정하는 것이 더 좋습니다. 자세한 내용은 서버 응답 Extras를 참조하세요.
  3. 요청을 재시도하면 결과가 오류인 경우 지수 백오프 사용. Google Play 클라이언트는 실패한 요청을 자동으로 재시도하므로 대부분의 경우 Policy에서 재시도할 필요가 없습니다.
  4. 라이선스 확인을 재시도하는 동안 사용자가 제한된 시간이나 사용 횟수로 애플리케이션에 액세스할 수 있는 '유예 기간' 제공. 유예 기간은 다음 라이선스 확인이 성공적으로 완료될 수 있을 때까지 액세스를 허용하여 사용자에게 도움이 되고 사용 가능한 유효한 라이선스 응답이 없을 때 애플리케이션 액세스를 엄격하게 제한하여 개발자에게도 도움이 됩니다.

위에 나열된 가이드라인에 따라 Policy를 설계하는 것이 중요합니다. 사용자에게 가능한 최고의 환경을 보장하면서 오류 상황에서도 애플리케이션을 효과적으로 제어할 수 있기 때문입니다.

Policy는 라이선스 서버에서 제공하는 설정을 사용하여 유효성 및 캐싱을 관리하고 유예 기간 재시도 등을 할 수 있습니다. 서버 제공 설정을 추출하는 것은 간단하므로 이 설정을 활용하는 것이 더 좋습니다. extras를 추출하고 사용하는 방법의 예는 ServerManagedPolicy 구현을 참조하세요. 서버 설정 목록 및 사용 방법에 관한 자세한 내용은 서버 응답 Extras를 참조하세요.

ServerManagedPolicy

LVL에는 ServerManagedPolicy라는 Policy 인터페이스의 권장되는 전체 구현이 포함되어 있습니다. 구현은 LVL 클래스와 통합되며 라이브러리에서 기본 Policy 역할을 합니다.

ServerManagedPolicy는 라이선스 및 재시도 응답을 모두 처리합니다. 응답 데이터를 모두 SharedPreferences 파일에 로컬로 캐시하여 애플리케이션의 Obfuscator 구현으로 난독화합니다. 이렇게 하면 라이선스 응답 데이터가 기기 전원을 껐다 켜는 동안 안전하게 지속됩니다. ServerManagedPolicy는 인터페이스 메서드인 processServerResponse()allowAccess()의 구체적인 구현을 제공하고 라이선스 응답을 관리하는 지원 메서드 및 유형 세트도 포함합니다.

무엇보다도 ServerManagedPolicy의 핵심 기능은 애플리케이션의 환불 기간에 걸쳐 다양한 네트워크 및 오류 상황에서 라이선스를 관리하는 기반으로 서버에서 제공한 설정을 사용하는 것입니다. 애플리케이션이 Google Play 서버에 라이선스 확인을 위해 접속하면 서버는 특정 라이선스 응답 유형의 extras 필드에 여러 설정을 키-값 쌍으로 추가합니다. 예를 들어 Google Play 서버는 애플리케이션의 라이선스 유효 기간, 재시도 유예 기간, 최대 허용 재시도 횟수 등의 권장 값을 제공합니다. ServerManagedPolicy는 processServerResponse() 메서드의 라이선스 응답에서 값을 추출하여 allowAccess() 메서드에서 확인합니다. ServerManagedPolicy에서 사용하는 서버 제공 설정 목록은 서버 응답 Extras를 참조하세요.

Google Play 서버의 라이선스 설정을 사용하는 편의성과 최고의 성능, 이점을 위해 라이선스 Policy으로 ServerManagedPolicy를 사용하는 것이 좋습니다.

SharedPreferences에 로컬로 저장된 라이선스 응답 데이터의 보안이 우려된다면 더 강력한 난독화 알고리즘을 사용하거나 라이선스 데이터를 저장하지 않는 더 엄격한 Policy를 디자인하면 됩니다. LVL에는 이러한 Policy의 예가 포함되어 있습니다. 자세한 내용은 StrictPolicy를 참고하세요.

ServerManagedPolicy를 사용하려면 활동으로 가져와서 인스턴스를 만들고 LicenseChecker를 구성할 때 인스턴스 참조를 전달하기만 하면 됩니다. 자세한 내용은 LicenseChecker 및 LicenseCheckerCallback 인스턴스화를 참조하세요.

StrictPolicy

LVL에는 StrictPolicy라는 Policy 인터페이스의 대체 전체 구현이 포함되어 있습니다. StrictPolicy 구현은 ServerManagedPolicy보다 더 제한적인 정책을 제공합니다. 그런 점에서 액세스 시점에 사용자가 라이선스를 부여받았다는 것을 표시하는 서버에서 수신한 라이선스 응답이 없다면 사용자는 애플리케이션에 액세스할 수 없습니다.

StrictPolicy의 주요 기능은 어떤 라이선스 응답 데이터도 영구 저장소에 로컬로 저장하지 않는다는 것입니다. 어떤 데이터도 저장되지 않으므로 재시도 요청이 추적되지 않으며 캐시된 응답을 사용하여 라이선스 확인을 실행할 수 없습니다. Policy는 다음 경우에만 액세스를 허용합니다.

  • 라이선스 응답이 라이선스 서버에서 수신됩니다.
  • 라이선스 응답은 사용자에게 애플리케이션 액세스 라이선스가 부여되었음을 나타냅니다.

가능한 모든 경우에서 사용자가 애플리케이션 사용 시점에 라이선스를 부여받았다고 확인되지 않는 경우 어떤 사용자도 애플리케이션에 액세스할 수 없도록 하는 것이 개발자의 주된 관심사라면 StrictPolicy 사용이 적절합니다. 또한 이 정책은 ServerManagedPolicy보다 보안이 약간 더 우수합니다. 로컬에 캐시된 데이터가 없으므로 악성 사용자가 캐시된 데이터를 조작하여 애플리케이션 액세스 권한을 얻을 수 있는 방법이 없습니다.

동시에 이 Policy는 일반 사용자에게 어렵습니다. 사용 가능한 네트워크(셀 또는 Wi-Fi) 연결이 없으면 애플리케이션에 액세스할 수 없기 때문입니다. 또 다른 부작용은 애플리케이션이 라이선스 확인 요청을 서버에 더 많이 전송하는 것입니다. 캐시된 응답을 사용하는 것이 불가능하기 때문입니다.

전반적으로 이 정책은 액세스의 완벽한 보안과 제어를 위해 사용자 편의성을 어느 정도 희생한 것입니다. 이 Policy를 사용하기 전에 이 사실을 신중하게 고려하세요.

StrictPolicy를 사용하려면 활동으로 가져와서 인스턴스를 만들고 LicenseChecker를 구성할 때 인스턴스 참조를 전달하기만 하면 됩니다. 자세한 내용은 LicenseChecker 및 LicenseCheckerCallback 인스턴스화를 참조하세요.

일반적인 Policy 구현은 애플리케이션의 라이선스 응답 데이터를 영구 저장소에 저장해야 애플리케이션 호출과 기기 전원을 껐다 켜는 동안 이 데이터에 액세스할 수 있습니다. 예를 들어 Policy는 애플리케이션이 실행될 때마다 값을 재설정하기보다는 성공적인 마지막 라이선스 확인의 타임스탬프, 재시도 횟수, 라이선스 유효 기간, 영구 저장소의 유사 정보를 유지합니다. LVL에 포함된 기본 Policy인 ServerManagedPolicy는 라이선스 응답 데이터를 SharedPreferences 인스턴스에 저장하여 데이터가 영구적인지 확인합니다.

Policy는 저장된 라이선스 응답 데이터를 사용하여 애플리케이션 액세스를 허용할지, 허용하지 않을지 판단하므로 저장된 데이터가 모두 안전하고 기기의 루트 사용자가 재사용하거나 조작할 수 없다고 보장해야 합니다. 특히 Policy는 애플리케이션과 기기에 고유한 키를 사용하여 데이터를 저장하기 전에 항상 난독화해야 합니다. 애플리케이션별 및 기기별 키를 사용하여 난독화하는 것이 중요합니다. 난독화된 데이터가 애플리케이션과 기기 사이에서 공유되는 것을 방지하기 때문입니다.

LVL은 애플리케이션이 라이선스 응답 데이터를 안전하고 영구적인 방식으로 저장하도록 지원합니다. 먼저 애플리케이션이 저장된 데이터에 선택한 난독화 알고리즘을 제공할 수 있는 Obfuscator 인터페이스를 제공합니다. 이를 기반으로 LVL은 도우미 클래스인 PreferenceObfuscator를 제공합니다. 이 클래스는 애플리케이션의 Obfuscator 클래스를 호출하고 SharedPreferences 인스턴스에 있는 난독화된 데이터를 읽고 쓰는 대부분의 작업을 처리합니다.

LVL은 AES 암호화를 사용하여 데이터를 난독 화하는 AESObfuscator라는 전체 Obfuscator 구현을 제공합니다. 애플리케이션에서 AESObfuscator를 수정 없이 사용하거나 필요에 맞게 조정할 수 있습니다. 라이선스 응답 데이터를 캐시하는 Policy(예: ServerManagedPolicy)를 사용하고 있다면 AESObfuscator를 Obfuscator 구현의 기반으로 사용하는 것이 좋습니다. 자세한 내용은 다음 섹션을 참조하세요.

AESObfuscator

LVL에는 AESObfuscator라는 Obfuscator 인터페이스의 권장되는 전체 구현이 포함되어 있습니다. 구현은 LVL 샘플 애플리케이션과 통합되며 라이브러리에서 기본 Obfuscator 역할을 합니다.

AESObfuscator는 AES를 사용하여 저장소에 쓰거나 저장소에서 읽을 때 데이터를 암호화 및 복호화하여 데이터의 안전한 난독화를 제공합니다. Obfuscator는 애플리케이션에서 제공하는 세 개의 데이터 필드를 사용하여 암호화를 시드합니다.

  1. 솔트 - 각 난독화 해제에 사용할 임의의 바이트 배열
  2. 일반적으로 애플리케이션의 패키지 이름인 애플리케이션 식별자 문자열
  3. 고유하게 만들기 위해 최대한 많은 기기별 소스에서 파생된 기기 식별자 문자열

AESObfuscator를 사용하려면 먼저 활동으로 가져옵니다. 비공개 정적 최종 배열을 선언하여 솔트 바이트를 보유하고 임의로 생성된 20바이트에 초기화합니다.

Kotlin

// Generate 20 random bytes, and put them here.
private val SALT = byteArrayOf(
        -46, 65, 30, -128, -103, -57, 74, -64, 51, 88,
        -95, -45, 77, -117, -36, -113, -11, 32, -64, 89
)

Java

...
    // Generate 20 random bytes, and put them here.
    private static final byte[] SALT = new byte[] {
     -46, 65, 30, -128, -103, -57, 74, -64, 51, 88, -95,
     -45, 77, -117, -36, -113, -11, 32, -64, 89
     };
    ...

그런 다음 변수를 선언하여 기기 식별자를 보유하고 필요한 어떤 방식으로든 그 값을 생성합니다. 예를 들어 LVL에 포함된 샘플 애플리케이션은 각 기기에 고유한 android.Settings.Secure.ANDROID_ID의 시스템 설정을 쿼리합니다.

사용하는 API에 따라 애플리케이션에서 기기별 정보를 얻기 위해 추가 권한을 요청해야 할 수도 있습니다. 예를 들어 TelephonyManager에 쿼리하여 기기 IMEI 또는 관련 데이터를 가져오려면 애플리케이션은 manifest에서 android.permission.READ_PHONE_STATE 권한도 요청해야 합니다.

Obfuscator에서 사용할 기기별 정보를 가져오기 위한 유일한 목적으로 새로운 권한을 요청하기 전에 가져오는 것이 애플리케이션 또는 Google Play의 필터링에 어떤 영향을 미칠 수 있는지 고려하세요. 일부 권한으로 인해 SDK 빌드 도구가 연결된 <uses-feature>를 추가할 수 있기 때문입니다.

마지막으로 AESObfuscator의 인스턴스를 구성하여 솔트, 애플리케이션 식별자 및 기기 식별자를 전달합니다. PolicyLicenseChecker를 구성하면서 인스턴스를 직접 구성할 수 있습니다. 예:

Kotlin

    ...
    // Construct the LicenseChecker with a Policy.
    private val checker = LicenseChecker(
            this,
            ServerManagedPolicy(this, AESObfuscator(SALT, packageName, deviceId)),
            BASE64_PUBLIC_KEY
    )
    ...

Java

    ...
    // Construct the LicenseChecker with a Policy.
    checker = new LicenseChecker(
        this, new ServerManagedPolicy(this,
            new AESObfuscator(SALT, getPackageName(), deviceId)),
        BASE64_PUBLIC_KEY // Your public licensing key.
        );
    ...

전체 예는 LVL 샘플 애플리케이션의 MainActivity를 참조하세요.

활동에서 라이선스 확인

애플리케이션 액세스를 관리하는 Policy를 구현하고 나면 다음 단계는 라이선스 확인을 애플리케이션에 추가하는 것입니다. 라이선스 확인은 필요에 따라 라이선스 서버에 쿼리를 시작하고 라이선스 응답에 기반하여 애플리케이션 액세스를 관리합니다. 라이선스 확인을 추가하고 응답을 처리하는 작업은 모두 기본 Activity 소스 파일에서 실행됩니다.

라이선스 확인을 추가하고 응답을 처리하려면 다음을 실행해야 합니다.

  1. 가져오기 추가
  2. 비공개 내부 클래스로 LicenseCheckerCallback 구현
  3. LicenseCheckerCallback에서 UI 스레드로 게시할 핸들러 만들기
  4. LicenseChecker 및 LicenseCheckerCallback 인스턴스화
  5. checkAccess()를 호출하여 라이선스 확인 시작
  6. 라이선스를 위한 공개 키 삽입
  7. LicenseChecker의 onDestroy() 메서드를 호출하여 IPC 연결 종료

아래 섹션에서는 이러한 작업을 설명합니다.

라이선스 확인 및 응답 개요

대부분의 경우 라이선스 확인을 onCreate() 메서드에서 애플리케이션의 기본 Activity에 추가해야 합니다. 이렇게 하면 사용자가 애플리케이션을 직접 실행할 때 라이선스 확인이 즉시 호출됩니다. 일부 경우에는 다른 위치에 라이선스 확인을 추가할 수도 있습니다. 예를 들어 애플리케이션에 다른 애플리케이션이 Intent로 시작할 수 있는 여러 활동 구성요소가 포함된 경우 그러한 활동에 라이선스 확인을 추가할 수도 있습니다.

라이선스 확인은 다음 두 가지 기본 작업으로 구성됩니다.

  • 라이선스 확인을 시작하는 메서드 호출 - LVL에서 이 호출은 개발자가 구성하는 LicenseChecker 객체의 checkAccess() 메서드 호출입니다.
  • 라이선스 확인 결과를 반환하는 콜백. LVL에서 이 콜백은 개발자가 구현하는 LicenseCheckerCallback 인터페이스입니다. 인터페이스는 두 가지 메서드 allow()dontAllow()를 선언합니다. 이 메서드는 라이선스 확인 결과에 기반하여 라이브러리에서 호출합니다. 필요한 모든 로직으로 이러한 두 가지 메서드를 구현하여 사용자의 애플리케이션 액세스를 허용하거나 허용하지 않습니다. 이러한 메서드는 액세스 허용여부를 판단하지 않습니다. 그 판단은 Policy 구현이 담당합니다. 오히려 이러한 메서드는 액세스를 허용 및 허용하지 않는 방법과 애플리케이션 오류를 처리하는 방법에 관한 애플리케이션 동작을 제공하기만 합니다.

    allow()dontAllow() 메서드는 Policy 값인 LICENSED, NOT_LICENSED 또는 RETRY 중 하나일 수 있는 응답의 '이유'를 제공합니다. 특히 메서드가 dontAllow()RETRY 응답을 수신하는 경우를 처리하고 사용자에게 '재시도' 버튼을 제공해야 합니다. 이러한 경우는 요청 중에 서비스를 사용할 수 없어서 발생했을 수 있습니다.

그림 1. 일반적인 라이선스 확인 상호작용 개요

위의 다이어그램은 일반적인 라이선스 확인이 어떻게 이루어지는지 보여줍니다.

  1. 애플리케이션의 기본 활동에 있는 코드에서 LicenseCheckerCallbackLicenseChecker 객체를 인스턴스화합니다. LicenseChecker를 구성할 때 이 코드는 Context, 사용할 Policy 구현 및 라이선스를 위한 게시자 계정의 공개 키를 매개변수로 전달합니다.
  2. 그런 다음 코드는 LicenseChecker 객체의 checkAccess() 메서드를 호출합니다. 메서드 구현은 Policy를 호출하여 SharedPreferences에서 로컬에 캐시된 유효한 라이선스 응답이 있는지 확인합니다.
    • 응답이 있다면 checkAccess() 구현에서 allow()를 호출합니다.
    • 응답이 없다면 LicenseChecker에서 라이선스 서버에 전송된 라이선스 확인 요청을 시작합니다.

    참고: 라이선스 서버는 임시 애플리케이션의 라이선스 확인을 실행할 때 항상 LICENSED를 반환합니다.

  3. 응답이 수신되면 LicenseChecker는 서명된 라이선스 데이터를 확인하고 응답 필드를 추출하는 LicenseValidator를 만든 후 추가 평가를 위해 Policy에 전달합니다.
    • 라이선스가 유효하면 PolicySharedPreferences에 응답을 캐시하고 검사기에 알립니다. 그러면 검사기는 LicenseCheckerCallback 객체의 allow() 메서드를 호출합니다.
    • 라이선스가 유효하지 않으면 Policy는 검사기에 알리고 검사기는 LicenseCheckerCallback에서 dontAllow() 메서드를 호출합니다.
  4. 요청을 전송할 네트워크를 사용할 수 없는 경우와 같이 복구 가능한 로컬 또는 서버 오류의 경우 LicenseCheckerPolicy 객체의 processServerResponse() 메서드에 RETRY 응답을 전달합니다.

    또한 allow()dontAllow() 콜백 메서드는 모두 reason 인수를 수신합니다. allow() 메서드의 이유는 일반적으로 Policy.LICENSED 또는 Policy.RETRY이고 dontAllow() 메서드의 이유는 일반적으로 Policy.NOT_LICENSED 또는 Policy.RETRY입니다. 이러한 응답 값은 사용자에게 적절한 응답을 표시할 수 있으므로 유용합니다. 예를 들어 dontAllow()Policy.RETRY로 응답할 때 '재시도' 버튼을 제공하는 방식 등입니다. 이러한 응답은 서비스를 사용할 수 없었기 때문일 수 있습니다.

  5. 애플리케이션이 잘못된 패키지 이름의 라이선스를 확인하려는 때와 같은 애플리케이션 오류의 경우 LicenseChecker는 오류 응답을 LicenseCheckerCallback의 applicationError() 메서드에 전달합니다.

아래 섹션에 설명된 라이선스 확인을 시작하고 결과를 처리하는 것 외에도 애플리케이션은 정책 구현을 제공해야 하며 Policy가 응답 데이터 (예: ServerManagedPolicy)를 저장하는 경우 Obfuscator 구현도 제공해야 합니다.

가져오기 추가

먼저 애플리케이션의 기본 활동 클래스 파일을 열고 LVL 패키지에서 LicenseCheckerLicenseCheckerCallback을 가져옵니다.

Kotlin

import com.google.android.vending.licensing.LicenseChecker
import com.google.android.vending.licensing.LicenseCheckerCallback

자바

import com.google.android.vending.licensing.LicenseChecker;
import com.google.android.vending.licensing.LicenseCheckerCallback;

LVL, ServerManagedPolicy와 함께 제공된 기본 Policy 구현을 사용하는 경우 AESObfuscator와 함께 ServerManagedPolicy도 가져옵니다. 맞춤 Policy 또는 Obfuscator를 사용한다면 대신 이를 가져오세요.

Kotlin

import com.google.android.vending.licensing.ServerManagedPolicy
import com.google.android.vending.licensing.AESObfuscator

Java

import com.google.android.vending.licensing.ServerManagedPolicy;
import com.google.android.vending.licensing.AESObfuscator;

비공개 내부 클래스로 LicenseCheckerCallback 구현

LicenseCheckerCallback은 라이선스 확인 결과를 처리하기 위해 LVL에서 제공하는 인터페이스입니다. LVL을 사용하여 라이선스를 지원하려면 LicenseCheckerCallback과 그 메서드를 구현하여 애플리케이션 액세스를 허용하거나 허용하지 않아야 합니다.

라이선스 확인 결과는 항상 LicenseCheckerCallback 메서드 중 하나의 호출입니다. 이 메서드는 응답 페이로드의 유효성 검사, 서버 응답 코드 자체 및 Policy에서 제공한 모든 추가 처리에 기반하여 만들어집니다. 애플리케이션은 필요한 어떤 방식으로든 이 메서드를 구현할 수 있습니다. 일반적으로 메서드를 단순하게 유지하여 UI 상태 및 애플리케이션 액세스를 관리하는 것으로 제한하는 것이 가장 좋습니다. 백엔드 서버에 문의하거나 맞춤 제약 조건을 적용하는 등의 방식으로 라이선스 응답 처리를 추가하려면 코드를 LicenseCheckerCallback 메서드에 배치하기보다는 Policy에 통합하는 것을 고려해야 합니다.

대부분의 경우 LicenseCheckerCallback 구현을 애플리케이션의 기본 활동 클래스 내부에 비공개 클래스로 선언해야 합니다.

allow()dontAllow() 메서드를 필요에 따라 구현하세요. 먼저 대화상자에 라이선스 결과를 표시하는 것과 같은 메서드에서 간단한 결과 처리 동작을 사용할 수 있습니다. 이렇게 하면 애플리케이션을 더 빨리 실행할 수 있고 디버깅을 지원할 수 있습니다. 나중에 정확히 원하는 동작을 파악한 후에는 더 복잡한 처리를 추가할 수 있습니다.

dontAllow()에서 라이선스가 부여되지 않은 응답을 처리하기 위한 몇 가지 제안은 다음과 같습니다.

  • 제공된 reasonPolicy.RETRY인 경우 새로운 라이선스 확인을 시작하는 버튼을 포함하는 '다시 시도' 대화상자를 사용자에게 표시합니다.
  • 사용자가 애플리케이션을 구매할 수 있는 Google Play의 애플리케이션 세부정보 페이지로 사용자를 딥 링크하는 버튼을 포함하는 '이 애플리케이션 구매' 대화상자를 표시합니다. 이러한 링크를 설정하는 방법에 관한 자세한 내용은 제품에 연결을 참조하세요.
  • 라이선스가 부여되지 않아 애플리케이션의 기능이 제한된다는 토스트 메시지 알림을 표시합니다.

아래 예는 LVL 샘플 애플리케이션이 대화상자에 라이선스 확인 결과를 표시하는 메서드와 함께 LicenseCheckerCallback를 구현하는 방법을 보여줍니다.

Kotlin

private inner class MyLicenseCheckerCallback : LicenseCheckerCallback {

    override fun allow(reason: Int) {
        if (isFinishing) {
            // Don't update UI if Activity is finishing.
            return
        }
        // Should allow user access.
        displayResult(getString(R.string.allow))
    }

    override fun dontAllow(reason: Int) {
        if (isFinishing) {
            // Don't update UI if Activity is finishing.
            return
        }
        displayResult(getString(R.string.dont_allow))

        if (reason == Policy.RETRY) {
            // If the reason received from the policy is RETRY, it was probably
            // due to a loss of connection with the service, so we should give the
            // user a chance to retry. So show a dialog to retry.
            showDialog(DIALOG_RETRY)
        } else {
            // Otherwise, the user isn't licensed to use this app.
            // Your response should always inform the user that the application
            // isn't licensed, but your behavior at that point can vary. You might
            // provide the user a limited access version of your app or you can
            // take them to Google Play to purchase the app.
            showDialog(DIALOG_GOTOMARKET)
        }
    }
}

Java

private class MyLicenseCheckerCallback implements LicenseCheckerCallback {
    public void allow(int reason) {
        if (isFinishing()) {
            // Don't update UI if Activity is finishing.
            return;
        }
        // Should allow user access.
        displayResult(getString(R.string.allow));
    }

    public void dontAllow(int reason) {
        if (isFinishing()) {
            // Don't update UI if Activity is finishing.
            return;
        }
        displayResult(getString(R.string.dont_allow));

        if (reason == Policy.RETRY) {
            // If the reason received from the policy is RETRY, it was probably
            // due to a loss of connection with the service, so we should give the
            // user a chance to retry. So show a dialog to retry.
            showDialog(DIALOG_RETRY);
        } else {
            // Otherwise, the user isn't licensed to use this app.
            // Your response should always inform the user that the application
            // isn't licensed, but your behavior at that point can vary. You might
            // provide the user a limited access version of your app or you can
            // take them to Google Play to purchase the app.
            showDialog(DIALOG_GOTOMARKET);
        }
    }
}

또한 applicationError() 메서드를 구현해야 합니다. LVL은 이 메서드를 호출하여 재시도할 수 없는 오류를 애플리케이션이 처리하도록 합니다. 이러한 오류 목록은 라이선스 참조에서 서버 응답 코드를 참조하세요. 이 메서드는 필요한 어떤 방식으로든 구현할 수 있습니다. 대부분의 경우 이 메서드는 오류 코드를 기록하고 dontAllow()를 호출해야 합니다.

LicenseCheckerCallback에서 UI 스레드로 게시할 핸들러 만들기

라이선스 확인 중에 LVL은 라이선스 서버와의 통신을 처리하는 Google Play 애플리케이션에 요청을 전달합니다. LVL은 비동기 IPC(Binder 사용)를 통해 요청을 전달하므로 실제 처리와 네트워크 통신은 애플리케이션이 관리하는 스레드에서 일어나지 않습니다. 마찬가지로 Google Play 애플리케이션이 결과를 수신하면 IPC를 통해 콜백 메서드를 호출하여 애플리케이션 프로세스의 IPC 스레드 풀에서 실행됩니다.

LicenseChecker 클래스는 요청을 전송하는 호출과 응답을 수신하는 콜백을 비롯하여 애플리케이션의 Google Play 애플리케이션과의 IPC 통신을 관리합니다. LicenseChecker는 오픈 라이선스 요청을 추적하고 시간 제한도 관리합니다.

애플리케이션의 UI 스레드에 영향을 미치지 않고 시간 제한을 올바르게 처리하고 들어오는 응답도 처리할 수 있도록 LicenseChecker는 인스턴스화 시 백그라운드 스레드를 생성합니다. 결과가 서버에서 수신된 응답이든, 시간 제한 오류이든 상관없이 백그라운드 스레드에서 라이선스 확인 결과를 모두 처리합니다. 처리가 끝나면 LVL은 백그라운드 스레드에서 LicenseCheckerCallback 메서드를 호출합니다.

애플리케이션에 이것은 다음을 의미합니다.

  1. LicenseCheckerCallback 메서드는 대부분의 경우에 백그라운드 스레드에서 호출됩니다.
  2. 이러한 메서드는 UI 스레드에서 핸들러를 만들어 핸들러에 콜백 메서드를 게시하지 않으면 UI 스레드에서 상태를 업데이트하거나 처리를 호출할 수 없습니다.

LicenseCheckerCallback 메서드가 UI 스레드를 업데이트하도록 하려면 아래와 같이 기본 활동의 onCreate() 메서드에서 Handler를 인스턴스화하세요. 이 예에서 LVL 샘플 애플리케이션의 LicenseCheckerCallback 메서드(위 참조)는 displayResult()를 호출하여 핸들러의 post() 메서드를 통해 UI 스레드를 업데이트합니다.

Kotlin

    private lateinit var handler: Handler

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        handler = Handler()
    }

Java

    private Handler handler;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...
        handler = new Handler();
    }

그런 다음 LicenseCheckerCallback 메서드에서 핸들러 메서드를 사용하여 Runnable 또는 메시지 객체를 핸들러에 게시할 수 있습니다. 다음은 LVL에 포함된 샘플 애플리케이션이 UI 스레드의 핸들러에 Runnable을 게시하여 라이선스 상태를 표시하는 방법입니다.

Kotlin

private fun displayResult(result: String) {
    handler.post {
        statusText.text = result
        setProgressBarIndeterminateVisibility(false)
        checkLicenseButton.isEnabled = true
    }
}

Java

private void displayResult(final String result) {
        handler.post(new Runnable() {
            public void run() {
                statusText.setText(result);
                setProgressBarIndeterminateVisibility(false);
                checkLicenseButton.setEnabled(true);
            }
        });
    }

LicenseChecker 및 LicenseCheckerCallback 인스턴스화

기본 활동의 onCreate() 메서드에서 LicenseCheckerCallback과 LicenseChecker의 비공개 인스턴스를 만드세요. LicenseCheckerCallback을 먼저 인스턴스화해야 합니다. LicenseChecker 생성자를 호출할 때 이 인스턴스 참조를 전달해야 하기 때문입니다.

LicenseChecker를 인스턴스화할 때 다음 매개변수를 전달해야 합니다.

  • 애플리케이션 Context
  • 라이선스 확인에 사용할 Policy 구현 참조. 대부분의 경우 LVL에서 제공하는 기본 Policy 구현인 ServerManagedPolicy를 사용합니다.
  • 라이선스를 위한 게시자 계정의 공개 키를 보유하는 문자열 변수

ServerManagedPolicy를 사용하는 경우 클래스에 직접 액세스할 필요가 없으므로 아래 예와 같이 LicenseChecker 생성자에서 클래스를 인스턴스화할 수 있습니다. ServerManagedPolicy를 구성할 때 새로운 Obfuscator 인스턴스 참조를 전달해야 합니다.

아래 예는 Activity 클래스의 onCreate() 메서드에서 LicenseCheckerLicenseCheckerCallback의 인스턴스화를 보여줍니다.

Kotlin

class MainActivity : AppCompatActivity() {
    ...
    private lateinit var licenseCheckerCallback: LicenseCheckerCallback
    private lateinit var checker: LicenseChecker

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        // Construct the LicenseCheckerCallback. The library calls this when done.
        licenseCheckerCallback = MyLicenseCheckerCallback()

        // Construct the LicenseChecker with a Policy.
        checker = LicenseChecker(
                this,
                ServerManagedPolicy(this, AESObfuscator(SALT, packageName, deviceId)),
                BASE64_PUBLIC_KEY // Your public licensing key.
        )
        ...
    }
}

Java

public class MainActivity extends Activity {
    ...
    private LicenseCheckerCallback licenseCheckerCallback;
    private LicenseChecker checker;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // Construct the LicenseCheckerCallback. The library calls this when done.
        licenseCheckerCallback = new MyLicenseCheckerCallback();

        // Construct the LicenseChecker with a Policy.
        checker = new LicenseChecker(
            this, new ServerManagedPolicy(this,
                new AESObfuscator(SALT, getPackageName(), deviceId)),
            BASE64_PUBLIC_KEY // Your public licensing key.
            );
        ...
    }
}

LicenseChecker는 로컬에 캐시된 유효한 라이선스 응답이 있는 경우에 UI 스레드에서 LicenseCheckerCallback 메서드를 호출합니다. 라이선스 확인이 서버로 전송되면 콜백은 네트워크 오류의 경우에도 항상 백그라운드 스레드에서 시작됩니다.

checkAccess()를 호출하여 라이선스 확인 시작

기본 활동에서 LicenseChecker 인스턴스의 checkAccess() 메서드 호출을 추가합니다. 호출에서 LicenseCheckerCallback 인스턴스 참조를 매개변수로 전달합니다. 호출 전에 특수한 UI 효과나 상태 관리를 처리해야 한다면 래퍼 메서드에서 checkAccess()를 호출하는 것이 유용할 수 있습니다. 예를 들어 LVL 샘플 애플리케이션은 doCheck() 래퍼 메서드에서 checkAccess()를 호출합니다.

Kotlin

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        // Call a wrapper method that initiates the license check
        doCheck()
        ...
    }
    ...
    private fun doCheck() {
        checkLicenseButton.isEnabled = false
        setProgressBarIndeterminateVisibility(true)
        statusText.setText(R.string.checking_license)
        checker.checkAccess(licenseCheckerCallback)
    }

Java

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // Call a wrapper method that initiates the license check
        doCheck();
        ...
    }
    ...
    private void doCheck() {
        checkLicenseButton.setEnabled(false);
        setProgressBarIndeterminateVisibility(true);
        statusText.setText(R.string.checking_license);
        checker.checkAccess(licenseCheckerCallback);
    }

라이선스를 위한 공개 키 삽입

각 애플리케이션에 Google Play 서비스는 라이선스 및 인앱 결제에 사용되는 2048비트 RSA 공개 키/비공개 키 쌍을 자동으로 생성합니다. 키 쌍은 애플리케이션과 고유하게 연결됩니다. 애플리케이션과 연결되어 있더라도 키 쌍은 애플리케이션에 서명하는 데 사용하거나 애플리케이션에서 파생된 키와 동일하지 않습니다.

Google Play Console은 라이선스를 위한 공개 키를 Play Console에 로그인한 모든 개발자에게 노출하지만 비공개 키는 안전한 위치에 보관하여 모든 사용자에게 공개하지 않습니다. 애플리케이션이 계정에 게시된 애플리케이션의 라이선스 확인을 요청하면 라이선스 서버는 애플리케이션 키 쌍의 비공개 키를 사용하여 라이선스 응답에 서명합니다. LVL이 응답을 수신하면 애플리케이션에서 제공한 공개 키를 사용하여 라이선스 응답의 서명을 확인합니다.

애플리케이션에 라이선스를 추가하려면 라이선스를 위한 애플리케이션의 공개 키를 가져와 애플리케이션에 복사해야 합니다. 다음은 라이선스를 위한 애플리케이션의 공개 키를 찾는 방법입니다.

  1. Google Play Console로 이동하여 로그인합니다. 라이선스를 부여할 애플리케이션이 게시되거나 게시될 계정으로 로그인해야 합니다.
  2. 애플리케이션 세부정보 페이지에서 서비스 및 API 링크를 찾아 클릭합니다.
  3. 서비스 및 API 페이지에서 라이선스 및 인앱 결제 섹션을 찾습니다. 라이선스를 위한 공개 키는 이 애플리케이션의 라이선스 키 필드에 있습니다.

애플리케이션에 공개 키를 추가하려면 필드의 키 문자열을 문자열 변수 BASE64_PUBLIC_KEY 값으로 애플리케이션에 복사하여 붙여넣기만 하면 됩니다. 복사할 때 빠뜨린 문자 없이 전체 키 문자열을 선택했는지 확인합니다.

다음은 LVL 샘플 애플리케이션의 예입니다.

Kotlin

private const val BASE64_PUBLIC_KEY = "MIIBIjANBgkqhkiG ... " //truncated for this example
class LicensingActivity : AppCompatActivity() {
    ...
}

Java

public class MainActivity extends Activity {
    private static final String BASE64_PUBLIC_KEY = "MIIBIjANBgkqhkiG ... "; //truncated for this example
    ...
}

LicenseChecker의 onDestroy() 메서드를 호출하여 IPC 연결 종료

마지막으로 애플리케이션 Context가 변경되기 전에 LVL을 정리하려면 활동의 onDestroy() 구현에서 LicenseCheckeronDestroy() 메서드를 추가로 호출합니다. 이 호출로 인해 LicenseChecker가 Google Play 애플리케이션의 ILicensingService에 열린 IPC 연결을 모두 올바르게 종료하며 서비스 및 핸들러의 모든 로컬 참조가 삭제됩니다.

LicenseCheckeronDestroy() 메서드 호출에 실패하면 애플리케이션 수명 주기에 걸쳐 문제가 발생할 수 있습니다. 예를 들어 라이선스 확인이 활성화된 상태에서 사용자가 화면 방향을 변경하면 애플리케이션 Context가 제거됩니다. 애플리케이션이 LicenseChecker의 IPC 연결을 올바르게 종료하지 않으면 애플리케이션은 응답이 수신될 때 비정상 종료됩니다. 마찬가지로 라이선스 확인이 진행되는 동안 사용자가 애플리케이션을 종료하면 응답이 수신될 때 애플리케이션이 비정상 종료됩니다. LicenseCheckeronDestroy() 메서드를 올바르게 호출하여 서비스에서 연결 해제된 경우는 예외입니다.

다음은 LVL에 포함된 샘플 애플리케이션의 예입니다. 여기에서 mCheckerLicenseChecker 인스턴스입니다.

Kotlin

    override fun onDestroy() {
        super.onDestroy()
        checker.onDestroy()
        ...
    }

Java

    @Override
    protected void onDestroy() {
        super.onDestroy();
        checker.onDestroy();
        ...
    }

LicenseChecker를 확장하거나 수정하고 있는 경우 모든 열린 IPC 연결을 정리하기 위해 LicenseCheckerfinishCheck() 메서드를 호출해야 할 수도 있습니다.

DeviceLimiter 구현

일부 경우에는 Policy가 단일 라이선스를 사용하도록 허용된 실제 기기의 수를 제한하는 것이 좋습니다. 이렇게 하면 사용자가 라이선스가 부여된 애플리케이션을 여러 기기로 이동하고 동일한 계정 ID로 그러한 기기에서 애플리케이션을 사용하는 것을 방지할 수 있습니다. 사용자가 라이선스와 연결된 계정 정보를 다른 개인에게 제공하여 애플리케이션을 '공유'하는 것도 방지할 수 있습니다. 제공받은 개인은 자신의 기기에서 이 계정으로 로그인하여 애플리케이션의 라이선스에 액세스할 수 있습니다.

LVL은 단일 메서드 allowDeviceAccess()를 선언하는 DeviceLimiter 인터페이스를 제공하여 기기별 라이선스를 지원합니다. LicenseValidator는 라이선스 서버의 응답을 처리할 때 allowDeviceAccess()를 호출하여 응답에서 추출된 사용자 ID 문자열을 전달합니다.

기기 제한을 지원하지 않으려면 어떤 작업도 필요하지 않습니다 - LicenseChecker 클래스가 NullDeviceLimiter라는 기본 구현을 자동으로 사용합니다. 이름에서 알 수 있듯이 NullDeviceLimiter는 allowDeviceAccess() 메서드가 모든 사용자와 기기에 LICENSED 응답을 반환하기만 하는 'no-op' 클래스입니다.

주의: 기기별 라이선스는 대부분의 애플리케이션에 권장되지 않습니다. 그 이유는 다음과 같습니다.

  • 백엔드 서버를 제공하여 사용자 및 기기 매핑을 관리해야 합니다.
  • 사용자가 합법적으로 다른 기기에서 구매한 애플리케이션에 사용자의 액세스가 실수로 거부될 수 있습니다.

코드 난독화

애플리케이션의 보안을 보장하려면 특히 라이선스 또는 맞춤 제약 조건 및 보호 조치를 사용하는 유료 애플리케이션의 경우 애플리케이션 코드를 난독화하는 것이 매우 중요합니다. 코드를 올바르게 난독화하면 악성 사용자가 애플리케이션의 바이트 코드를 디컴파일하고 수정(예: 라이선스 확인 삭제)한 다음 다시 컴파일하기가 더 어려워집니다.

여러 obfuscator 프로그램을 Android 애플리케이션에 사용할 수 있습니다. 예를 들면 코드 최적화 기능도 제공하는 ProGuard가 있습니다. Google Play 라이선스를 사용하는 모든 애플리케이션에는 ProGuard 또는 유사 프로그램을 사용하여 코드를 난독화하는 것이 좋습니다.

라이선스가 부여된 애플리케이션 게시

라이선스 구현 테스트를 완료하면 Google Play에 애플리케이션을 게시할 수 있습니다. 일반적인 단계에 따라 준비하고 서명한 다음 애플리케이션을 게시하세요.

지원받을 수 있는 곳

애플리케이션에서 게시를 구현하거나 배포하는 동안 질문이 있거나 문제가 발생하면 아래 표에 나열된 지원 리소스를 사용하세요. 쿼리를 올바른 포럼에 보내면 필요한 지원을 더욱 신속하게 받을 수 있습니다.

표 2. Google Play 라이선스 서비스의 개발자 지원 리소스

지원 유형 리소스 주제 범위
개발 및 테스트 문제 Google 그룹스: android-developers LVL 다운로드 및 통합, 라이브러리 프로젝트, Policy 질문, 사용자 환경 아이디어, 응답 처리, Obfuscator, IPC, 테스트 환경 설정
스택 오버플로: http://stackoverflow.com/questions/tagged/android
계정, 게시 및 배포 문제 Google Play 도움말 포럼 게시자 계정, 라이선스 키 쌍, 테스트 계정, 서버 응답, 테스트 응답, 애플리케이션 배포 및 결과
마켓 라이선스 지원 FAQ
LVL Issue Tracker Marketlicensing 프로젝트 Issue Tracker 특히 LVL 소스 코드 클래스 및 인터페이스 구현과 관련된 버그 및 문제 신고

위에 나열된 그룹에 게시하는 방법에 관한 일반적인 내용은 개발자 지원 리소스 페이지의 커뮤니티 리소스 섹션을 참조하세요.

추가 리소스

LVL에 포함된 샘플 애플리케이션은 MainActivity 클래스에서 라이선스 확인을 시작하고 그 결과를 처리하는 방법에 관한 전체 예를 제공합니다.