2013/06/04

Google Play の Licensing サービスをデバッグする

Google Play の Licensing サービスを使う にて Licensing サービスを利用する準備が整いましたので、実際に動作させ、デバッグしてみます。

テスト用アカウントの種類

まずは、実際に購入していなくても、ライセンス認証に通るアカウントを設定します。
テストレスポンスを受信可能なアカウントは以下のように2種類あります。
用途によって使い分けましょう。

アップロード前にチェック可テストレスポンス受信可テスト設定可
Publisher account
Test account××
Other×××

Publisher Account の設定

Publisher Account はアカウント所有者とほぼ同等の権限を持つアカウントです。
Licensing サービスの設定だけでなく、APK のアップロードやストアの掲載情報の編集、アプリの削除まで出来てしまいますので、お気をつけください。

まず、アカウント所有者で Developer Console にログインし、
設定 → ユーザーアカウントと権限 → 新しいユーザーを招待

Gmail アドレスを入力して、「招待状を送信」をクリックすると、その Gmail アカウントにメールが届きます。
その指示に従うと、Publisher アカウントとして登録されます。

Test account の設定

Test Account は Licensing サービスに特化したテストアカウントです。
Publisher Account のように多くの権限がないため、大人数で開発を行なっている場合には使用しやすいでしょう。
ただ、アップロード前のアプリに対してデバッグが出来ないという制限があります。

Developer Console → 設定 → アカウントの詳細

「テスト用のアクセス権がある Gmail アカウント」 に gmail アドレスを入力。
複数ある場合はカンマ区切りで

デバッグ

「ライセンステスト応答」を設定し、保存します。
先のアプリでライセンス認証を行うと以下の様なレスポンスが返ってきます当然、テストアカウントでログインしている端末で動作させる必要があります。
ServerManagedPolicy を適用している場合の Callback を載せておきましたので参考にして下さい。

ServerManagedPolicy の Callback
LICENSEDallow
LICENSED_OLD_KEYallow
NOT_LICENSEDdontAllow
ERROR_CONTACTING_SERVERdontAllow (Retry)
ERROR_SERVER_FAILUREdontAllow (Retry)
ERROR_INVALID_PACKAGE_NAMEapplicationError
ERROR_NON_MATCHING_UIDapplicationError
ERROR_NOT_MARKET_MANAGEDapplicationError

さらに詳しい情報は以下を参考にして下さい。

以上でデバッグ完了です。

ServerManagedPolicy を使う場合の注意

ServerManagedPolicy を適用した場合、一度 LICENSED で認証が返ると、その情報をローカルキャッシュに保存してしまいます。

キャッシュを使用しているかどうかは、LogCat を見るとわかります。
以下のように Using cached license response が出力されている場合はキャッシュを使っています。
この場合、一定時間たたないと、常に allow が返ってきます。

以下のように Received response. が出力されている場合は通信した結果を使用しています。


2013/06/03

Google Play の Licensing サービスを使う

Google Play には、そのアプリが正規に Google Play からダウンロードされ、そのアカウントに利用権限があるかどうか確認するためのサービスがあります。
これにより、 「有料アプリをダウンロード。その後、すぐ apk をSDカード等にコピーして、購入をキャンセル。すると無料でアプリを使用出来る」 という裏技が(簡単には)使用できなくなるので、有料アプリを公開している方は是非導入すべきでしょう。

ちなみに、LVL (License Verification Library) というと通りが良いようですが、LVL は Licensing Service を Android アプリに組み込む時に使用するライブラリのことです。

2013/06/02

SQLiteDatabase.openDatabase() に OPEN_READONLY 属性を付けているのに "attempt to write a readonly database" が発生することがある

私が GooglePlay に出しているアプリ SQLiteViewer に以下のようなクラッシュレポートが届きました。

java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=0, result=-1, data=Intent { dat=xxxx }} to activity {com.kokufu.android.apps.sqliteviewer.free/com.kokufu.android.apps.sqliteviewer.base.MainActivity}: android.database.sqlite.SQLiteException: attempt to write a readonly database
at android.app.ActivityThread.deliverResults(ActivityThread.java:2532)
at android.app.ActivityThread.handleSendResult(ActivityThread.java:2574)
at android.app.ActivityThread.access$2000(ActivityThread.java:117)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:961)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:130)
at android.app.ActivityThread.main(ActivityThread.java:3683)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:507)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:897)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:655)
at dalvik.system.NativeStart.main(Native Method)
Caused by: android.database.sqlite.SQLiteException: attempt to write a readonly database
at android.database.sqlite.SQLiteDatabase.dbopen(Native Method)
at android.database.sqlite.SQLiteDatabase.(SQLiteDatabase.java:1849)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:820)
at com.kokufu.android.apps.sqliteviewer.base.b.a(ProGuard:28)
at com.kokufu.android.apps.sqliteviewer.base.MainActivity.c(ProGuard:631)
at com.kokufu.android.apps.sqliteviewer.base.MainActivity.a(ProGuard:471)
at com.kokufu.android.apps.sqliteviewer.base.MainActivity.onActivityResult(ProGuard:213)
at android.app.Activity.dispatchActivityResult(Activity.java:3935)
at android.app.ActivityThread.deliverResults(ActivityThread.java:2528)
... 11 more

発生したのは SQLiteDatabase.openDatabase()。
この手のアプリは、開こうとしたファイルによってはエラーが出てしまうのは仕方の無いことですとは言え、クラッシュして良いわけではないので、最新版ではクラッシュしないように修正済です。
SQLiteException が RuntimeException なのは間違いだと思う。


ただ、よく見てみると、Caused by のところに、attempt to write a readonly database との文字が。
コードの該当箇所は以下で、OPEN_READONLY 属性がついているのにも関わらずです。

            db = SQLiteDatabase.openDatabase(
                    uri.getPath(),
                    null,
                    SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);

「そんなわけないだろー」と思いながら検索してみると、以下のような情報が。

なるほど。LG-P500 (Android 2.3.3) にあるバグなのね。
で、以下のようにすれば回避可能と。

try {
    db = SQLiteDatabase.openDatabase(
            uri.getPath(),
            null,
            SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
} catch (SQLiteException e) {
    final String message = e.getMessage();
    if (message == null) {
       throw e;
     }
     if (!message.contains("attempt to write a readonly database")) {
       throw e;
     }
    // データベースを read-only モードで開こうとしたのにも関わらず、
    // 失敗することがあります。
    // これは、LG-P500 Release:2.3.3 Sdk:10. にあるバグのためです。
    // openDatabase メソッドは readonly で開いたデータベースに
    // 書き込みを行おうとします。
    // これは1度だけのようなので、1回 readwrite モードで開いた後、
    // 閉じて、さらにもう1度 readonly で開きます。
    db = SQLiteDatabase.openDatabase(
            uri.getPath(),
            null,
            SQLiteDatabase.OPEN_READWRITE);
    db.close();
    db = SQLiteDatabase.openDatabase(
            uri.getPath(),
            null,
            SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
}

何か、特定の機種のバグ回避のために、READWRITE でデータベースを開くのは気が進まないんですけど、仕方ありません。
まー、そもそも、クラッシュレポートに機種情報が載っていなかったので、LG-P500 の問題だったのかどうかもわからないのですが…