Home>Freeware for Android - ライセンス認証(LVL)の組み込み -



 何となく有料のアプリも作る様になっておいた方が良いかな、と言う事でライセンス認証(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 のテストしか出来ません。


Home>Freeware for Android - Eclipse から AndroidStudio へ移行 - inserted by FC2 system inserted by FC2 system