6 月 3 日の「#Android11: The Beta Launch Show」にぜひご参加ください。

ライセンス リファレンス

LVL のクラスとインターフェース

表 1 に、Android SDK で利用可能な License Verification Library(LVL)のすべてのソースファイルを示します。ファイルはすべて、com.android.vending.licensing パッケージに含まれています。

表 1. LVL のクラスとインターフェースの概要

カテゴリ 名前 説明
ライセンス チェックと結果 LicenseChecker ライセンス チェックを開始するためにインスタンス化(またはサブクラス化)するクラス。
LicenseCheckerCallback ライセンス チェックの結果を処理するために実装するインターフェース。
ポリシー Policy ライセンス応答に基づいて、アプリの使用を許可するかどうかを決定するために実装するインターフェース。
ServerManagedPolicy デフォルトの Policy 実装。ライセンス サーバーから提供される設定を使用して、ライセンス データのローカル ストレージ、ライセンスの有効性、再試行を管理します。
StrictPolicy Policy 実装のデフォルト以外の選択肢。サーバーからのライセンス応答のみに基づく使用可否判断を適用します。キャッシュ保存やリクエストの再試行は行いません。
データの難読化
(任意)
Obfuscator ライセンス応答データを永続ストアにキャッシュ保存する Policy(ServerManagedPolicy など)を使用する場合に実装するインターフェース。難読化アルゴリズムを適用して、書き込みや読み取りを行うデータのエンコードとデコードを行います。
AESObfuscator AES による暗号化と復号のアルゴリズムを使用してデータの難読化と難読化解除を行うデフォルトの Obfuscator 実装。
デバイスの制限
(任意)
DeviceLimiter アプリの使用を特定のデバイスに限定する場合に実装するインターフェース。LicenseValidator から呼び出されます。DeviceLimiter の実装は、バックエンド サーバーを必要とするうえ、慎重に設計しないとユーザーがライセンスされたアプリを使用できなくなる可能性があるため、ほとんどのアプリには推奨されません。
NullDeviceLimiter デフォルトの DeviceLimiter 実装で、何も制限しません(すべてのデバイスで使用を許可します)。
ライブラリの主要部(統合は不要) ResponseData ライセンス応答のフィールドを保持するクラス。
LicenseValidator ライセンス サーバーから受信した応答を復号して検証するクラス。
ValidationException Obfuscator で管理されるデータの完全性を検証する際に発生するエラーを示すクラス。
PreferenceObfuscator システムの SharedPreferences ストアに対して難読化データの読み書きを行うユーティリティ クラス。
ILicensingService 一方向 IPC インターフェース。ライセンス チェック リクエストはこのインターフェースを介して Google Play クライアントに渡されます。
ILicenseResultListener 一方向 IPC コールバック実装。アプリはこの実装を介してライセンス サーバーから非同期応答を受け取ります。

サーバー応答コード

表 2 に、ライセンス サーバーがサポートしているすべてのライセンス応答コードを示します。通常、アプリではこれらの応答コードをすべて処理できなければなりません。デフォルトでは、LVL の LicenseValidator クラスには、これらの応答コードに対する必要な処理がすべて含まれています。

表 2. Google Play サーバーからのライセンス応答で返される応答コードの概要

応答コード 説明 署名付き 付加情報 備考
LICENSED ユーザーにアプリのライセンスが付与されています。そのユーザーは、アプリを購入しているか、アプリのアルファ版またはベータ版をダウンロードしてインストールする権限を有しています。 はい VTGTGR Policy の制約に基づいて使用を許可します。
LICENSED_OLD_KEY ユーザーにアプリのライセンスが付与されていますが、異なる鍵で署名された更新版のアプリがリリースされています。 はい VTGTGRUT Policy の制約に基づいて、必要に応じて使用を許可します。

インストールされているアプリのバージョンで使用している鍵ペアは無効、または信頼できないことを示している可能性があります。アプリでは、必要に応じて使用を許可することができます。または、アップグレード可能であることをユーザーに通知し、アップグレードされるまで使用を制限することもできます。

NOT_LICENSED ユーザーにアプリのライセンスが付与されていません。 いいえ 使用を許可しません。
ERROR_CONTACTING_SERVER ローカルエラー。Google Play アプリからライセンス サーバーへの通信ができませんでした。おそらく、ネットワークを利用できないことが原因です。 いいえ Policy の再試行制限内でライセンス チェックを再試行します。
ERROR_SERVER_FAILURE サーバーエラー。サーバーでアプリのライセンス用鍵ペアを読み込めませんでした。 いいえ Policy の再試行制限内でライセンス チェックを再試行します。
ERROR_INVALID_PACKAGE_NAME ローカルエラー。デバイスにインストールされていないパッケージのライセンス チェックがアプリからリクエストされました。 いいえ ライセンス チェックを再試行しないでください。

通常、開発エラーが原因です。

ERROR_NON_MATCHING_UID ローカルエラー。アプリからリクエストされたライセンス チェックの対象パッケージの UID(パッケージ、ユーザー ID のペア)が、リクエスト元アプリの UID と一致しません。 いいえ ライセンス チェックを再試行しないでください。

通常、開発エラーが原因です。

ERROR_NOT_MARKET_MANAGED サーバーエラー。アプリ(パッケージ名)が Google Play で認識されませんでした。 いいえ ライセンス チェックを再試行しないでください。

アプリが Google Play を介して公開されていないか、ライセンスの実装に開発エラーがあることを示している可能性があります。

注: テスト環境のセットアップに記載されているとおり、アプリ デベロッパーと登録済みテストユーザー向けの応答コードは、Google Play Console を使用して手動でオーバーライドできます。

注: 以前は、公開前のドラフト版をアップロードしてアプリをテストできました。この機能はサポートされなくなったため、代わりに、アルファ版またはベータ版の配布チャネルにアプリを公開する必要があります。詳細については、ドラフト版アプリのサポート終了をご覧ください。

サーバー応答の付加情報

アプリの払い戻し期間全体にわたってアプリの使用を管理できるようにアプリをサポートし、また、上記以外の情報を提供するために、ライセンス応答にはライセンス サーバーによっていくつかの情報が付加されます。具体的には、ライセンス サービスから、アプリのライセンス有効期間、再試行猶予期間、最大許容再試行回数などの推奨設定値が提供されます。アプリで APK 拡張ファイルを使用している場合は、応答にファイル名、サイズ、URL も含まれます。サーバーはこれらの設定を、ライセンス応答の「付加情報」フィールドに Key-Value ペアとして追加します。

Policy 実装では、ライセンス応答から付加情報の設定を抽出し、必要に応じて使用できます。LVL のデフォルトの Policy 実装(ServerManagedPolicy)は、そのまま機能する実装としても、付加情報設定の取得、保存、使用の方法を示す例としても利用できます。

表 3. Google Play サーバーからのライセンス応答で提供されるライセンス管理設定の概要

付加情報説明
VT ライセンス有効性タイムスタンプ。現在の(キャッシュ保存された)ライセンス応答が失効し、ライセンス サーバーで再チェックする必要がある日時を示します。ライセンス有効期間については、下記のセクションをご覧ください。
GT 猶予期間タイムスタンプ。Policy でアプリの使用を許可できる期間の終了時を示します(応答ステータスが RETRY の場合も含む)。

値はサーバーによって管理されますが、通常は 5 日以上です。再試行期間と最大再試行回数については、下記のセクションをご覧ください。

GR 最大再試行回数。ユーザーによるアプリの使用を拒否する前に、Policy で許可すべきライセンス チェックの連続 RETRY 数を示します。

値はサーバーによって管理されますが、通常は「10」以上です。再試行期間と最大再試行回数については、下記のセクションをご覧ください。

UT 更新タイムスタンプ。このアプリの最新版がアップロードされ公開された日時を示します。

この付加情報は、LICENSED_OLD_KEYS 応答でのみ返されます。Policy はこの情報を基に、ユーザーによるアプリの使用を拒否する前に、新しいライセンス キーでアップデートが公開されてからどの程度経過しているかを確認できます。

FILE_URL1またはFILE_URL2 拡張ファイルの URL(1 はメインファイル、2 はパッチファイル)。HTTP でファイルをダウンロードする場合はこの URL を使用します。
FILE_NAME1またはFILE_NAME2 拡張ファイルの名前(1 はメインファイル、2 はパッチファイル)。ファイルをデバイスに保存する場合は、この名前を使用する必要があります。
FILE_SIZE1またはFILE_SIZE2 バイト単位のファイルサイズ(1 はメインファイル、2 はパッチファイル)。この情報により、ファイルをダウンロードする前にデバイスの共有ストレージに十分な空き容量があるかどうかを確認できます。

ライセンス有効期間

Google Play ライセンス サーバーにより、ダウンロードされたすべてのアプリに対してライセンス有効期間が設定されます。この期間は、アプリ内のライセンス Policy で、アプリのライセンス ステータスが不変かつキャッシュ保存可能と見なすべき時間間隔を表します。すべてのライセンス チェックに対する応答には、ライセンス サーバーにより、付加情報のキー VT の値として有効期限のタイムスタンプが追加されます。これにより、応答から有効期間がわかります。Policy では、VT キーの値を抽出して使用することにより、有効期間が終了するまでという条件付きで、ライセンスを再チェックせずにアプリの使用を許可できます。

ライセンス Policy でライセンス ステータスをライセンス サーバーに再チェックする必要がある場合は、ライセンス有効性の通知が行われます。これは、実際にアプリ使用のライセンスが付与されているかどうかを示すものではありません。つまり、アプリのライセンス有効期間が終了しても、アプリにライセンスが付与されなくなったわけではありません。Policy でサーバーのライセンス ステータスを再チェックする必要があるというだけです。したがって、Policy では、最初のライセンス ステータスをローカルにキャッシュ保存し、ライセンス有効期間が終了するまでの間は、ライセンス チェックをサーバーに送信する代わりに、キャッシュ保存したライセンス ステータスを返すことができます。

ライセンス サーバーによる有効期間の管理は、Google Play で提供される有料アプリの払い戻し期間にわたって、アプリでライセンスを適切に適用できるようにするための手段です。有効期間は、アプリが購入されたかどうか、また購入された場合はどれくらい前に購入されたかに基づいて設定されます。サーバーでの有効期間の設定は、具体的には次のように行われます。

  • 有料アプリの場合、サーバーは最初のライセンス有効期間を設定し、アプリが払い戻し可能な間はライセンス応答が有効なままになるようにします。これにより、アプリのライセンス Policy では、最初のライセンス チェックの結果をキャッシュ保存しておけば、その有効期間が終了するまで再度ライセンス チェックを行わずに済みます。
  • アプリの払い戻しができなくなると、サーバーはより長い有効期間(通常は数日)を設定します。
  • 無料アプリの場合、サーバーは有効期間を非常に大きい値(long.MAX_VALUE)に設定します。これにより、Policy では、有効期間のタイムスタンプをローカルにキャッシュ保存しておけば、以後アプリのライセンス ステータスを再チェックする必要がなくなります。

ServerManagedPolicy 実装では、抽出したタイムスタンプ(mValidityTimestamp)を、ユーザーにアプリの使用を許可する前にライセンス ステータスをサーバーに再チェックするかどうか決めるための主要な条件として使用します。

再試行期間と最大再試行回数

場合によっては、システムやネットワークの状態により、アプリからのライセンス チェックがライセンス サーバーに届かない、またはサーバーからの応答が Google Play クライアント アプリに届かないことがあります。たとえば、ユーザーは、モバイル ネットワークやデータ接続が利用できない場合(飛行機の中にいる場合など)や、ネットワーク接続が不安定だったり、モバイルデータ信号が弱かったりする場合にも、アプリを起動する可能性があります。

ネットワークの問題によってライセンス チェックが正常に行われなかった場合は、Google Play クライアントから PolicyprocessServerResponse() メソッドに RETRY 応答コードが返され、アプリに通知されます。システムに問題がある場合(アプリが Google Play の ILicensingService 実装にバインドできない場合など)は、LicenseChecker ライブラリ自体により、RETRY 応答コードを使用して Policy の processServerResonse() メソッドが呼び出されます。

通常、RETRY 応答コードは、ライセンス チェックの完了を妨げるエラーが発生したことをアプリに通知するものです。

Google Play サーバーにより設定される再試行「猶予期間」と推奨最大再試行回数は、エラー状態のライセンス機能をアプリで管理する場合に役立ちます。これらの値は、サーバーによって、すべてのライセンス チェック応答の付加情報 GTGR のキーの下に追加されます。

アプリの Policy では、付加情報 GT および GR を抽出し、それらを使用して、次のように条件付きでアプリの使用を許可できます。

  • ライセンス チェックの結果として RETRY 応答が返された場合は、PolicyRETRY 応答コードをキャッシュ保存し、RETRY 応答のカウントをインクリメントします。
  • 再試行猶予期間がまだ有効であるか、最大再試行回数に達していない場合は、Policy でユーザーにアプリの使用を許可します。

ServerManagedPolicy では、サーバーから提供される上記の GTGR の値を使用します。下記の例は、allow() メソッド内での RETRY 応答の条件付き処理を示しています。RETRY 応答のカウントは、ここには示されていませんが、processServerResponse() メソッド内で維持されます。

Kotlin

    fun allowAccess(): Boolean {
        val ts = System.currentTimeMillis()
        return when(lastResponse) {
            LICENSED -> {
                // Check if the LICENSED response occurred within the validity timeout.
                ts <= validityTimestamp  // Cached LICENSED response is still valid.
            }
            RETRY -> {
                ts < lastResponseTime + MILLIS_PER_MINUTE &&
                        // Only allow access if we are within the retry period
                        // or we haven't used up our max retries.
                        (ts <= retryUntil || retryCount <= maxRetries)
            }
            else -> false
        }
    }
    

Java

    public boolean allowAccess() {
        long ts = System.currentTimeMillis();
        if (lastResponse == LicenseResponse.LICENSED) {
            // Check if the LICENSED response occurred within the validity timeout.
            if (ts <= validityTimestamp) {
                // Cached LICENSED response is still valid.
                return true;
            }
        } else if (lastResponse == LicenseResponse.RETRY &&
                    ts < lastResponseTime + MILLIS_PER_MINUTE) {
            // Only allow access if we are within the retry period
            // or we haven't used up our max retries.
            return (ts <= retryUntil || retryCount <= maxRetries);
        }
        return false;
    }