何となく有料のアプリも作る様になっておいた方が良いかな、と言う事でライセンス認証(LVL)を入れてみました。
Google公式にSDKツールでソースが提供されているのですが、極悪な事に素のままではまともに動きません。
めちゃ手間がかかったので、以下に自分用手順を置いておきます。
かなり前からまともに動かない状態で放置されているけど、世の有料アプリ開発者は皆同じ手間をかけているのだろうか?
■事前情報
先人有識者の情報。
ヒコザレポート [開発者向け] Android 有料アプリでライセンス認証(LVL)まとめ
必要な情報の全容と関連するリンクが集約されているので、ここを参照するだけで何とかなります。
まずは上記を熟読するのが吉。
詳細は上記に書かれているので、以下では AndroidStudio / targetSdkVersion 30 でやらなければいけない事だけを中心に書きます。
■LVLのダウンロード
SDKマネージャから Licensing library をダウンロードする。
ダウンロードしたファイルは 〜\android-sdk\extras\google\market_licensing 配下に格納されます。ここまでは簡単・・・
■LVLのインポート
AndroidStudio で組み込むアプリのプロジェクトを開き ファイル > 新規 > モジュールのインポート で、上記の library ディレクトリをインポート。
モジュール名は何でもいいけど「lvl」にするのが一般的? 以後の内容も「lvl」の前提で記載します。
すべてデフォルトで可。
暫くの間ビルド処理?が動いて ANDROID PROJECT IMPORT SUMMARY が出力される、こんな内容なら問題なし。
●app/build.gradle に compile project(':lvl') を追加する。
dependencies {
:
compile project(':lvl')
:
}
●app/src/main/AndroidManifest.xml に権限を追加する。
:
<uses-permission android:name="com.android.vending.CHECK_LICENSE"/>
:
ここまでやって ビルド>プロジェクトの作成 を動かすとエラーが沢山出る・・・ いろいろと提供ソースの手直しが必要。
●lvl/build.gradle の minSdkVersion=3 → 4 に修正。
●lvl/src/main/java/com/google/android/vending/licensing の LicenseChecker.java 149行目〜 を以下の様に修正。
:
/* コメントアウト
boolean bindResult = mContext
.bindService(
new Intent(
new String(
Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))),
this, // ServiceConnection.
Context.BIND_AUTO_CREATE);
コメントアウト */
// 置き換え
Intent intent = new Intent(new String(Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")));
// 明示的にパッケージ名を指定する。
intent.setPackage("com.android.vending");
boolean bindResult = mContext
.bindService(
intent,
this, // ServiceConnection.
Context.BIND_AUTO_CREATE);
// 置き換え
:
●lvl/src/main/AndroidManifest.xml
以下を追記
<application>
<uses-library android:name="org.apache.http.legacy" android:required="false" />
</application>
以下を削除
<uses-sdk android:minSdkVersion="3" android:targetSdkVersion="15" />
●lvl/build.gradle に以下を追記
android {
:
useLibrary 'org.apache.http.legacy'
:
}
ここまで修正して、Gradleファイルとプロジェクトを同期、プロジェクトの作成、するとエラーは無くなる筈。
以下の Warning が出るが無視して良い。
Configuration 'compile' is obsolete and has been replaced with 'implementation' and 'api'.
■ライセンス認証処理追加
認証したいアプリに認証判定を組み込む。
SDKツールからダウンロードした 〜android-sdk\extras\google\market_licensing\sample を参考にしても良いが、こっちの方が分かりやすい。
穀風 Google Play の Licensing サービスを使う |実装 のサンプルソースを参照
やる事の概要は・・・
・ライセンスキーを指定(後述■有料アプリの登録と試験の通り、PlayStoreに有料登録しないとキーが決定しない)
・mChecker.checkAccess(mLicenseCheckerCallback); で LicenseChecker を呼び出し。
・LicenseCheckerCallback でチェック結果を受け取り、
・allow メソッドには認証成功時の処理を追加する。
・dontAllow、applicationError メソッドには認証失敗時の処理(アプリを強制停止する等)を追加する。
56行目は誤記有 if (policyReason == Policy.RETRY) { が正しい。
サンプルの日本語コメントを見ると applicationError メソッドは起こりえない様にも見えるけど、そんな事は無いので applicationError にも認証失敗時処理の追加必要。
これで最低限度は動くけど、動かしてみると不足有り。
以下は過去の Web に記載は見当たらなかった(多分)けど、追加が必要だと思う内容。
●applicationError のエラーコード
多分、初期のデバッグでは applicationError に入る事が多いと思うが、サンプルソースはそこが空なので何が起きているか分からない。
例えば PlayStore のアプリ登録が正しくないと ERROR_NOT_MARKET_MANAGED になるが、LVL側の処理もそこで Log を出して無いので何も起きて無い様に見える。
以下のログ出力を追加すべき。
:
import com.google.android.vending.licensing.LicenseCheckerCallback;
:
public void applicationError(int errorCode) {
switch(errorCode){
case ERROR_INVALID_PACKAGE_NAME:
Log.v("xxx","LVL ERROR_INVALID_PACKAGE_NAME");
break;
case ERROR_NON_MATCHING_UID:
Log.v("xxx","LVL ERROR_NON_MATCHING_UID");
break;
case ERROR_NOT_MARKET_MANAGED:
Log.v("xxx","LVL ERROR_NOT_MARKET_MANAGED");
break;
case ERROR_CHECK_IN_PROGRESS:
Log.v("xxx","LVL ERROR_CHECK_IN_PROGRESS");
break;
case ERROR_INVALID_PUBLIC_KEY:
Log.v("xxx","LVL ERROR_INVALID_PUBLIC_KEY");
break;
case ERROR_MISSING_PERMISSION:
Log.v("xxx","LVL ERROR_MISSING_PERMISSION");
break;
default:
Log.v("xxx","LVL ERROR OTHER " + errorCode);
}
//アプリを停止する処理を入れる
}
:
●VT、GT、GR
過去の Web を見ると VT、GT、GR のキーワードが頻出するが、どこから取得するのか良く分からない。
LicenseCheckerCallback の戻り値には存在しない。
LVLのソースを眺めた結果として、どうやら
・VT = PREF_VALIDITY_TIMESTAMP 次にチェックする日時(認証成功から24h後、その間の認証は全て成功で返却される)
・GT = PREF_RETRY_UNTIL リトライ期限(認証成功から7日?後)
・GR = PREF_MAX_RETRIES
の事らしい・・・
PREF_〜 なので SharedPreferences に存在するのだが・・・
見事に暗号化されてます。(関係ないけど Rooted は SharedPreferences を参照できるのが有難い)
LVL の PreferenceObfuscator で暗号化されているので、同様に PreferenceObfuscator で複合化すれば値が取れます。以下サンプル。
:
public static final String PREF_LAST_RESPONSE = "lastResponse";
public static final String PREF_VALIDITY_TIMESTAMP = "validity_Timestamp";
public static final String PREF_RETRY_UNTIL = "retryUntil";
public static final String PREF_MAX_RETRIES = "maxRetries";
public static final String PREF_RETRY_COUNT = "retryCount";
public static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
public static final String DEFAULT_RETRY_UNTIL = "0";
public static final String DEFAULT_MAX_RETRIES = "0";
public static final String DEFAULT_RETRY_COUNT = "0";
:
public void log_lvl(){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss z");
Timestamp timestamp;
String deviceId = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
//SharedPreferences名は LVLのソースと合わせる
SharedPreferences sp = getSharedPreferences("com.android.vending.licensing.ServerManagedPolicy", Context.MODE_PRIVATE);
//AESObfuscator の引数は LicenseChecker 生成の引数と合わせる
PreferenceObfuscator mPreferences = new PreferenceObfuscator(sp, new AESObfuscator(SALT, getPackageName(), deviceId));
Log.v("xxx","mLastResponse = " + mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
long mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP, DEFAULT_VALIDITY_TIMESTAMP));
timestamp = new Timestamp( mValidityTimestamp );
Log.v("xxx","mValidityTimestamp = " + sdf.format(timestamp));
long mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
timestamp = new Timestamp( mRetryUntil );
Log.v("xxx","mRetryUntil = " + sdf.format(timestamp));
long mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
Log.v("xxx","mMaxRetries = " + mMaxRetries);
long mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
Log.v("xxx","mRetryCount = " + mRetryCount);
}
正しく動作すると、こんな値が入ってくる。入ってない場合は正しく動作していない。
:
2021/06/10 19:14:15 150 mLastResponse = 256 ← OK:0x100 NG:0x231 RETRY:0x123
2021/06/10 19:14:15 153 mValidityTimestamp = 2021/06/11 07:42:00 JST ← 6/10 7:42 に最後のチェックを行った
2021/06/10 19:14:15 156 mRetryUntil = 2021/06/17 07:42:00 JST
2021/06/10 19:14:15 159 mMaxRetries = 10
2021/06/10 19:14:15 162 mRetryCount = 0
:
PREF_VALIDITY_TIMESTAMP の時刻前に通信を切って認証出来なくすると RETRY:0x123 が返る。
:
2021/06/11 07:50:02 115 mLastResponse = 291
2021/06/11 07:50:02 117 mValidityTimestamp = 2021/06/11 07:42:00 JST ← 変わらない
2021/06/11 07:50:02 119 mRetryUntil = 2021/06/17 07:42:00 JST
2021/06/11 07:50:02 120 mMaxRetries = 10
2021/06/11 07:50:02 122 mRetryCount = 1
:
2021/06/11 07:50:12 141 mLastResponse = 291
2021/06/11 07:50:12 143 mValidityTimestamp = 2021/06/11 07:42:00 JST ← 変わらない
2021/06/11 07:50:12 145 mRetryUntil = 2021/06/17 07:42:00 JST
2021/06/11 07:50:12 146 mMaxRetries = 10
2021/06/11 07:50:12 148 mRetryCount = 2 ← 失敗する度に1増える(10を越えてもエラーにはなる訳ではない)
:
●policyReason == Policy.RETRY
Policy.RETRY が返却された時に、どう判定すれば良いか? どこにも明確に書いてない気がする。
〜android-sdk\extras\google\market_licensing\sample はリトライMSGを出しているが、その状況でアプリが使えなくなるのはちょっと違うのでは?
いろいろ試した結果で見ると
System.currentTimeMillis() が PREF_RETRY_UNTIL の値より小さい時は OK。大きい時は NG(処理停止)にすれば良いでしょう。
■AntiLVL対策
むらかみの雑記帳 AntiLVL への簡単な対抗方法について考えてみる *対策方法 を参照
補足)AndroidStudio / targetSdkVersion 30 で生成した APK は、何の対策無しでも AntiLVL はエラーが出て動きませんでした。
■有料アプリの登録と試験
PlayConsole は画面構成が頻繁に変わります・・・ 以下は 2021/6頃の画面。
まずは共通設定として「お支払いプロファイル」を作成。
「お受け取り方法」で振込先口座等を指定。
テスト時に自分でお金を払うのは無駄(手数料を取られる)なので「ライセンステスト」で自分をテストユーザ登録する。
後は PlayConsole にアプリを登録。登録する時に有料/無料を聞いて来るので「有料」として登録します。
一度無料で登録したアプリを有料に変更する事はできません。有料を無料に変更する事は可能。
登録すると「収益化のセットアップ」で認証用のキーが払い出されます。これを BASE64_PUBLIC_KEY に指定する。
テスト方法は過去の Web に色々やり方が書いてありましたが、面倒なので製品版リリースしてテスト。
エミュレータは Retry ステータスしか返って来ませんでした。Retry のテストしか出来ません。