ラベル In-app billing v3 の投稿を表示しています。 すべての投稿を表示
ラベル In-app billing v3 の投稿を表示しています。 すべての投稿を表示

2014年11月29日土曜日

AndroidアプリのIn-app Billing version2からversion3への更新 ③テスト


In-app Billing version3の最終回です。
今回は作ったものがまともに動くかテストする方法を確認します。

テストには二通りの方法があり、ある程度の動きはサンドボックステストで確認し、最終的に実購入テストという流れかと思います。


  • サンドボックステスト
サンドボックステストのために、以下4つの予約済みアイテムIDが用意されています。

購入成功(android.test.purchased)
キャンセル、クレジットカード無効など(android.test.canceled)
払い戻し(android.test.refunded)
存在しないアイテム(android.test.item_unavailable)

android.test.canceledandroid.test.refundedの場合は、RESPONSE_CODEBILLING_RESPONSE_RESULT_USER_CANCELEDあたりが返ってくるのかと思ったのですが、BILLING_RESPONSE_RESULT_OKが返ってきます。

ただし、INAPP_PURCHASE_DATAINAPP_DATA_SIGNATUREはnullです。

android.test.item_unavailableの場合は、onActivityResultに渡されてくるresultCodeRESULT_CANCELEDが返ってきます。

こっちのActivityのRESULT_CANCELEDは0で、RESULT_OKが-1です。In-app Billingで返ってくるレスポンスコードはBILLING_RESPONSE_RESULT_OKが0なのでややこしいです。


ちなみにandroid.test.purchasedで一度購入成功すると、二度と購入出来なくなるのですが、「設定→アプリ→Google Play→データを削除」でデータを削除すると、再度購入出来るようになります。

  • 実購入テスト
サンドボックステストに飽きたら実購入テストに入ります。
実購入テストでは、Google Playと実際に通信して購入処理をテストします。


アルファ版配布用に、google グループで適当にグループを作成し、テスト用のgmailアドレスを招待しておきます。

テスト購入で課金が発生したりしないように、Developer Consoleにログインし、テストアカウントを設定します。
「設定→アカウントの詳細→ライセンス テスト→テスト用のアクセス権がある Gmail アカウント」にテスト用のgmailアドレスを入力します。

テストしたいapkをリリース用のKeyStoreで署名して、アルファ版テストにアップロードします。

「テスターのリストを管理→アプリのアルファ テスト版を提供するユーザー」という画面で先ほど作成したグループを登録すると、オプトインのためのアドレスが表示されます。

テスターが「テスターになる」をタップすると設定は完了で、あとは実際にGoogle Playから実機にインストールして試すだけです。

ただ、アルファ版が有効になるまで結構時間がかかる(半日とか1日とか)ようなので、作ってはアップロード作ってはアップロードという試し方は出来なそうな雰囲気です。

とは言え、やはり課金関連で失敗するとひどいことになるので、十分なテストの上で公開したいところです。

時間を置くと、実際にGoogle Playからダウンロード出来るようになります。

ダウンロードしたアルファ版のアプリで購入ボタンを押すと、しっかりとテスト注文である旨が表示されます。
(アプリ名やカード番号は消しました)

アルファテストで購入完了すると、Googleウォレット販売者コンソールでは以下のように「追加済み」と表示されます。

ということで、苦手意識のあったIn-app Billingですが、version3になって本当にすっきり分かりやすくなりました。

食わず嫌いだった人はこれを機に、是非再チャレンジしてみると良いと思います。


Read More...

AndroidアプリのIn-app Billing version2からversion3への更新 ②レスポンスを受け取る


前回はIn-app Billing version3のリクエスト送信方法について触りましたので、今回はそのレスポンス処理方法についてです。

  • レスポンスを受け取る
startIntentSenderForResultのレスポンスはActivityのonActivityResultで受け取ります。

startIntentSenderForResultの第2引数で渡したリクエストコードとの一致を確認した上で、resultCoderesponseCodeが問題ないか確認し、レスポンスの中身を取り出します。

purchaseDataはJSON形式になっていて、その中のproductIdを確認すると、どの商品が購入されたかが分かります。

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE) {
        int responseCode = data.getIntExtra("RESPONSE_CODE", -1);
        if (resultCode == RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
            String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
            if(purchaseData != null) {
                String productId = "";
                try {
                    JSONObject jsonObj = new JSONObject(purchaseData);
                    productId = jsonObj.optString("productId");
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                //必要な処理
            }
        }
    }
}

ちなみに、getIntExtradefaultValueを0にしているサンプルを良く見かけるのですが、0だとBILLING_RESPONSE_RESULT_OKになってしまって微妙な気がしたので-1にしてみました。
なんか理由があるのかもしれませんが。

  • 署名の確認
テストアカウントであればサンドボックステストでも署名が返ってくるはずですが、うまくいかなかったので実購入テストで試しました。

登場人物はonActivityResultに渡されてきたIntentに入っている、INAPP_PURCHASE_DATAINAPP_DATA_SIGNATUREと、公開鍵であるBASE64_ENCODED_PUBLIC_KEYです。

BASE64_ENCODED_PUBLIC_KEYは、Developers Consoleで確認出来る公開鍵です。

INAPP_PURCHASE_DATAはレスポンスのjson文字列で、INAPP_DATA_SIGNATUREはそれを公開鍵で署名したものです。

署名検証にはjava.security.Signatureを利用し、流れとしてはSignaturegetInstanceメソッドで生成し、initVerifyメソッドで公開鍵で初期化。updateメソッドでレスポンスのjson文字列をバイト配列に変換したものを渡しverifyメソッドで検証します。

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE) {
        int responseCode = data.getIntExtra("RESPONSE_CODE", -1);
        if (resultCode == RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
            String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
            String signatureData = data.getStringExtra("INAPP_DATA_SIGNATURE");
            if (purchaseData != null && signatureData != null) {
                if(verify(generatePublicKey(BASE64_ENCODED_PUBLIC_KEY),purchaseData, signatureData)) {
                    //署名OK
                }
            }
        }
    }
}

private final String SIGNATURE_ALGORITHM = "SHA1withRSA";
private final String KEY_FACTORY_ALGORITHM = "RSA";

public boolean verify(PublicKey publicKey, String signedData, String signature) {
    Signature sig;
    try {
        sig = Signature.getInstance(SIGNATURE_ALGORITHM);
        sig.initVerify(publicKey);
        sig.update(signedData.getBytes());
        if (!sig.verify(Base64.decode(signature, Base64.DEFAULT))) {
            return false;
        }
        return true;
    } catch (InvalidKeyException e) {
    } catch (NoSuchAlgorithmException e) {
    } catch (SignatureException e) {
    }
    return false;
}

public PublicKey generatePublicKey(String encodedPublicKey) {
    PublicKey pKey = null;
    try {
        byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
        pKey = keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
    } catch (NoSuchAlgorithmException e) {
    } catch (InvalidKeySpecException e) {
    }
    return pKey;
}

署名のところはもう少し色々と考えなくてはいけないこともありますが、とりあえずは検証出来ました。
次回はここまで作成したもののテスト方法についてです。

Read More...

AndroidアプリのIn-app Billing version2からversion3への更新 ①リクエストを送る



GoogleがIn-app Billing Version 2 APIを殺すそうなので、もう長らく更新していなかったAndroidアプリを更新する羽目になっています。

昔version2の頃にアプリ内課金を実装したときは、正直意味が分からないけどサンプル通りに書いたら動いたというレベルだったので、結構苦手意識があります。

ただ、すごくシンプルになったという話も聞くので、これを機にまとめてみようと思います。


  • AIDLファイルの追加
サンプルアプリTrivialDriveに含まれている、Android Definititon Language(AIDL)ファイルを利用し、インターフェースファイルIIAppBillingService.javaを作ります。

Android SDK ManagerでGoogle Play Billing Libraryをインストールします 。


<android-sdk>/extras/google/play_billing/samples/TrivialDrive/からサンプルプロジェクトをインポートします。

TrivialDrivaサンプルの/srcディレクトリからIInAppBillingService.aidlファイルをコピーし、自分のアプリの/srcディレクトリへ貼り付けます。

/genディレクトリ内にIInAppBillingService.javaが自動的に作成されます。

  • AndroidManifestの更新
これはversion2と変わっていないようです。

AndroidManifest.xmlに以下のパーミッションを追加します。

<uses-permission android:name="com.android.vending.BILLING" />

  • ServiceConnectionの作成とIInAppBillingServiceへのバインド
接続時と切断時のコールバックメソッドを持つServiceConnectionを実装し、bindServiceメソッドでバインドします。

ServiceConnectionを実装します。

private IInAppBillingService mIInAppBillingService;
private ServiceConnection mServiceConnection= new ServiceConnection() {
    @Override
    public void onServiceDisconnected(ComponentName name) {
        mIInAppBillingService = null;
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mIInAppBillingService = IInAppBillingService.Stub.asInterface(service);
    }
};

IInAppBillingServiceを使用できるようにするため、ActivityonCreateメソッドでbindServiceメソッドを呼びます。
Intentの引数は、クラス名のIInAppBillingServiceではなく、InAppBillingServiceなのが紛らわしいですね。

Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE);

終了処理として、Activityが終了時にonDestroyメソッドでunbindServiceメソッドでアンバインドします。

if (mServiceConnection != null) {
    unbindService(mServiceConnection);
}


なお、このあたりで大きくハマったのですが、bindServiceを呼んでからServiceConnectiononServiceConnectedが呼ばれるまでにはタイムラグがあり、直後にIInAppBillingServiceをいじくろうとするとぬるぽで死にます。

bindService自体はtrueで返ってくるのに、IInAppBillingServiceはnullのまま。

時間を計ってみたところ、50~150ミリ秒後ぐらいでまちまちでしたが、何にしろタイムラグがあります。

直後にスレッド止めたりして待ち合わせをしても、ほぼ同時に返ってくるものの結局ぬるぽ。

まぁ何のためにonServiceConnectedメソッドを実装する必要があるのか考えれば、分かりそうなものでしたが。。

ということで、わざわざコールバックメソッドを実装させられている訳なので、onServiceConnectedからトリガーしてあげればぬるぽにはならない訳です。

  • サポート状況の確認
version3のAPIをサポートしているか確認します。

int response = mIInAppBillingService.isBillingSupported(3, getPackageName(), "inapp");
if(response == BILLING_RESPONSE_RESULT_OK) {
    //OKの場合の処理
}

  • 購入状況の確認
getPurchasesメソッドを呼ぶと、購入済みアイテムの情報を取得出来ます。

RESPONSE_CODEや、INAPP_PURCHASE_ITEM_LISTなど、いくつかの情報を含むBundleが返されるので、必要に応じて取り出し利用します。

Bundle bundle = mIInAppBillingService.getPurchases(3, getPackageName(), "inapp", null);
if(bundle.getInt("RESPONSE_CODE") == BILLING_RESPONSE_RESULT_OK) {
    ArrayList<String> purchases = bundle.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
    for (int i = 0, n = purchases.size(); i < n; i++) {
        //purchases.get(i)の中身によって処理を変える。
    }
}

  • 購入処理
getBuyIntentメソッドを呼んで、返されたBundleからBUY_INTENTキーのPendingIntentを取得します。
取得したPendingIntenstartIntentSenderForResultメソッドを呼び出します。

getBuyIntentメソッドの第4引数はDeveloper Payloadというやつで、空文字でもnullでもレスポンス時には空文字が返ってきます。
本来は任意の文字列を入れて、海賊版対策に使います。

startIntentSenderForResultメソッドの第2引数はコールバックを受けたときにリクエストを識別するための任意のint値です。

Bundle buyIntentBundle = mIInAppBillingService.getBuyIntent(3,
        getPackageName(), itemId, "inapp", "");
if (buyIntentBundle.getInt("RESPONSE_CODE") == BILLING_RESPONSE_RESULT_OK) {
    PendingIntent pendingIntent = buyIntentBundle
        .getParcelable("BUY_INTENT");
    startIntentSenderForResult(pendingIntent.getIntentSender(),
        REQUEST_CODE, new Intent(), Integer.valueOf(0),
        Integer.valueOf(0), Integer.valueOf(0));
}

なお、サポート状況の確認から購入処理まで、IInAppBillingServiceのメソッドを呼ぶ際には、RemoteExceptionが発生するので、適時catchします。

あとはActivityのonActivityResultへレスポンスが来るので、適時処理するだけです。


次回はstartIntentSenderForResultのレスポンス部分、onActivityResultでの処理についてです。


Read More...