LVL のクラスとインターフェース
表 1 に、Android SDK で利用可能な License Verification Library(LVL)のすべてのソースファイルを示します。ファイルはすべて、com.android.vending.licensing
パッケージに含まれます。
カテゴリ | 名前 | 説明 |
---|---|---|
ライセンス チェックと結果 | 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 に、ライセンス サーバーから返されるすべてのライセンス応答フィールドの一覧を示します。
フィールド | 説明 |
---|---|
responseCode |
ライセンス サーバーから返される応答コード。応答コードの概要については、サーバー応答コードをご覧ください。 |
signedData |
ライセンス サーバーから返されるデータを保持する文字列の連結(例: responseCode|nonce|packageName|versionCode|userId|timestamp:extras )。
|
signature |
アプリ固有の鍵を使用した signedData の署名。
|
サーバー応答コード
表 3 に、ライセンス サーバーがサポートしているすべてのライセンス応答コードの一覧を示します。通常、アプリではこれらの応答コードをすべて処理する必要があります。デフォルトでは、LVL の LicenseValidator クラスには、これらの応答コードに対する必要な処理がすべて含まれています。
応答コード | 整数値の表現 | 説明 | 署名付き | 付加情報 | 備考 |
---|---|---|---|---|---|
LICENSED |
0 |
ユーザーにアプリのライセンスが付与されています。ユーザーは、アプリを購入しているか、アプリのアルファ版またはベータ版をダウンロードしてインストールする権限を有しています。 | はい | VT 、GT 、GR |
Policy の制約に基づいてアクセスを許可します。 |
LICENSED_OLD_KEY |
2 |
ユーザーにアプリのライセンスが付与されていますが、異なる鍵で署名された更新版のアプリがリリースされています。 | はい | VT 、GT 、GR 、UT |
Policy の制約に基づいて、必要に応じてアクセスを許可します。
インストールされているアプリのバージョンで使用している鍵ペアが無効である、または不正使用されている可能性があります。アプリでは、必要に応じてアクセスを許可できます。または、アップグレード可能であることをユーザーに通知し、アップグレードされるまでアクセスを制限することもできます。 |
NOT_LICENSED |
1 |
ユーザーにアプリのライセンスが付与されていません。 | いいえ | アクセスを許可しません。 | |
ERROR_CONTACTING_SERVER |
257 |
ローカルエラー。Google Play アプリからライセンス サーバーに通信できませんでした。おそらく、ネットワークを利用できないことが原因です。 | いいえ | Policy の再試行制限内でライセンス チェックを再試行します。 |
|
ERROR_SERVER_FAILURE |
4 |
サーバーエラー。サーバーがアプリのライセンス用鍵ペアを読み込めませんでした。 | いいえ | Policy の再試行制限内でライセンス チェックを再試行します。
|
|
ERROR_INVALID_PACKAGE_NAME |
258 |
ローカルエラー。デバイスにインストールされていないパッケージのライセンス チェックがアプリからリクエストされました。 | いいえ | ライセンス チェックを再試行しないでください。
通常、開発エラーが原因です。 |
|
ERROR_NON_MATCHING_UID |
259 |
ローカルエラー。アプリからリクエストされたライセンス チェックの対象パッケージの UID(パッケージ、ユーザー ID のペア)が、リクエスト元アプリの UID と一致しません。 | いいえ | ライセンス チェックを再試行しないでください。
通常、開発エラーが原因です。 |
|
ERROR_NOT_MARKET_MANAGED |
3 |
サーバーエラー。アプリ(パッケージ名)が Google Play で認識されませんでした。 | いいえ | ライセンス チェックを再試行しないでください。
アプリが Google Play を介して公開されていないか、ライセンスの実装に開発エラーがある可能性があります。 |
注: テスト環境のセットアップに記載されているとおり、アプリ デベロッパーと登録済みテストユーザー向けの応答コードは、Google Play Console を使用して手動でオーバーライドできます。
注: 以前は、公開前のドラフト版をアップロードしてアプリをテストできました。この機能はサポートされなくなったため、代わりに、アルファ版またはベータ版の配布チャンネルにアプリを公開する必要があります。詳しくは、ドラフト版アプリのサポートの終了についての説明をご覧ください。
サーバー応答の付加情報
アプリの払い戻し期間全体にわたってアプリへのアクセスを管理できるようにアプリをサポートし、また、上記以外の情報を提供するために、ライセンス応答にはライセンス サーバーによっていくつかの情報が付加されます。具体的には、ライセンス サービスから、アプリのライセンス有効期間、再試行猶予期間、最大許容再試行回数などの推奨設定値が提供されます。アプリで APK 拡張ファイルを使用している場合は、応答にファイルの名前、サイズ、URL も含まれます。サーバーはこれらの設定を、ライセンス応答の「付加情報」フィールドに Key-Value ペアとして追加します。
Policy
実装では、ライセンス応答から付加情報の設定を抽出し、必要に応じて使用できます。LVL のデフォルトの Policy
実装(ServerManagedPolicy
)は、そのまま機能する実装としても、付加情報設定の取得、保存、使用の方法を示す例としても利用できます。
付加情報 | 説明 |
---|---|
VT |
ライセンス有効性タイムスタンプ。現在の(キャッシュに保存された)ライセンス応答の有効期限となり、ライセンス サーバーで再チェックする必要がある日時を示します。ライセンス有効期間については、下記のセクションをご覧ください。 |
GT |
猶予期間タイムスタンプ。Policy でアプリへのアクセスを許可できる期間の終了時を示します(応答ステータスが RETRY の場合も含む)。値はサーバーによって管理されますが、通常は 5 日以上です。再試行期間と最大再試行回数については、下記のセクションをご覧ください。 |
GR |
最大再試行回数。ユーザーによるアプリへのアクセスを拒否する前に、Policy で許可すべきライセンス チェックの連続 RETRY 数を示します。
値はサーバーによって管理されますが、通常は「10」以上です。再試行期間と最大再試行回数については、下記のセクションをご覧ください。 |
UT |
更新タイムスタンプ。このアプリの最新版がアップロードされ公開された日時を示します。 この付加情報は、 |
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 クライアントから Policy
の processServerResponse()
メソッドに RETRY
応答コードが返され、アプリに通知されます。システムに問題がある場合(アプリが Google Play の ILicensingService
実装にバインドできない場合など)は、LicenseChecker
ライブラリ自体が、RETRY
応答コードを使用して Policy の processServerResponse()
メソッドを呼び出します。
通常、RETRY
応答コードは、ライセンス チェックの完了を妨げるエラーが発生したことをアプリに通知するものです。
Google Play サーバーにより設定される再試行「猶予期間」と推奨最大再試行回数に基づいて、アプリはエラー状態のライセンス機能を管理することができます。これらの値は、サーバーによって、すべてのライセンス チェック応答の付加情報として GT
キーと GR
キーに追加されます。
アプリの Policy
では、付加情報 GT
と GR
を抽出し、それらを使用して、次のように条件付きでアプリへのアクセスを許可できます。
- ライセンス チェックの結果として
RETRY
応答が返された場合は、Policy
でRETRY
応答コードをキャッシュに保存し、RETRY
応答をカウントします。 - 再試行猶予期間内であるか、最大再試行回数に達していない場合は、
Policy
でユーザーにアプリへのアクセスを許可します。
ServerManagedPolicy
では、サーバーから提供される上記の GT
と GR
の値を使用します。下記の例は、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; }