Implementing In-app Billingがv3になったのでサンプルコードを適当に読んでみた。
■購入処理
MainActivityのレイアウトファイルのボタンは以下のようにImageViewで記述してある。
<ImageView android:id="@+id/infinite_gas_button" android:src="@drawable/infinite_gas" android:onClick="onInfiniteGasButtonClicked" android:layout_width="wrap_content" android:layout_height="wrap_content" />
onInfiniteGasButtonClickedメソッド
まず以下の処理でデバイスが購読に対応してるか確認。
if (!mHelper.subscriptionsSupported()) { complain("Subscriptions not supported on your device yet. Sorry!"); return; }
IabHelper.java
サービスを操作したりしているクラスで以下の部分で、マーケットアプリのバージョンを確認してbooleanをセットしている。
mServiceConn = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { logDebug("Billing service disconnected."); mService = null; } @Override public void onServiceConnected(ComponentName name, IBinder service) { logDebug("Billing service connected."); mService = IInAppBillingService.Stub.asInterface(service); String packageName = mContext.getPackageName(); try { logDebug("Checking for in-app billing 3 support."); // check for in-app billing v3 support int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP); if (response != BILLING_RESPONSE_RESULT_OK) { if (listener != null) listener.onIabSetupFinished(new IabResult(response, "Error checking for billing v3 support.")); // if in-app purchases aren't supported, neither are subscriptions. mSubscriptionsSupported = false; return; } logDebug("In-app billing version 3 supported for " + packageName); // check for v3 subscriptions support response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS); if (response == BILLING_RESPONSE_RESULT_OK) { logDebug("Subscriptions AVAILABLE."); mSubscriptionsSupported = true; } else { logDebug("Subscriptions NOT AVAILABLE. Response: " + response); } mSetupDone = true; } catch (RemoteException e) { if (listener != null) { listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION, "RemoteException while setting up in-app billing.")); } e.printStackTrace(); return; } if (listener != null) { listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful.")); } } };
以下の部分から購入処理が始まる。
/* TODO: for security, generate your payload here for verification. See the comments on * verifyDeveloperPayload() for more info. Since this is a SAMPLE, we just use * an empty string, but on a production app you should carefully generate this. */ String payload = ""; setWaitScreen(true);// 画面切り替えの際の表示上の処理 Log.d(TAG, "Launching purchase flow for infinite gas subscription."); mHelper.launchPurchaseFlow(this, SKU_INFINITE_GAS, IabHelper.ITEM_TYPE_SUBS, RC_REQUEST, mPurchaseFinishedListener, payload);
IabHelper.javaのlaunchPurchaseFlowメソッド
マーケットアプリのバージョン・購読に対応しているか確認する。
checkSetupDone("launchPurchaseFlow"); flagStartAsync("launchPurchaseFlow"); IabResult result; if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) { IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE, "Subscriptions are not available."); if (listener != null) listener.onIabPurchaseFinished(r, null); return; }
getBuyIntentでサービスに対して購入用のインテント生成処理を要求する。購入用のIntentを引き数にしてstartIntentSenderForResultをコールして実際に購入処理をする。
try { logDebug("Constructing buy intent for " + sku + ", item type: " + itemType); Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData); int response = getResponseCodeFromBundle(buyIntentBundle); if (response != BILLING_RESPONSE_RESULT_OK) { logError("Unable to buy item, Error response: " + getResponseDesc(response)); result = new IabResult(response, "Unable to buy item"); if (listener != null) listener.onIabPurchaseFinished(result, null); return; } PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode); mRequestCode = requestCode; mPurchaseListener = listener; mPurchasingItemType = itemType; act.startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, new Intent(), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0)); }
購読完了処理
正しくリクエストがされるとonActivityResultが呼ばれる。
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(TAG, "onActivityResult(" + requestCode + "," + resultCode + "," + data); // Pass on the activity result to the helper for handling if (!mHelper.handleActivityResult(requestCode, resultCode, data)) { // not handled, so handle it ourselves (here's where you'd // perform any handling of activity results not related to in-app // billing... super.onActivityResult(requestCode, resultCode, data); } else { Log.d(TAG, "onActivityResult handled by IABUtil."); } }
handleActivityResult
色々とチェックしてるのだが最終的に以下のコードが呼ばれる。
if (mPurchaseListener != null) { mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase); }
プロパティに保持してUIを更新しているだけっぽい。
// Callback for when a purchase is finished IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() { public void onIabPurchaseFinished(IabResult result, Purchase purchase) { Log.d(TAG, "Purchase finished: " + result + ", purchase: " + purchase); if (result.isFailure()) { complain("Error purchasing: " + result); setWaitScreen(false); return; } if (!verifyDeveloperPayload(purchase)) { complain("Error purchasing. Authenticity verification failed."); setWaitScreen(false); return; } Log.d(TAG, "Purchase successful."); if (purchase.getSku().equals(SKU_GAS)) { // bought 1/4 tank of gas. So consume it. Log.d(TAG, "Purchase is gas. Starting gas consumption."); mHelper.consumeAsync(purchase, mConsumeFinishedListener); } else if (purchase.getSku().equals(SKU_PREMIUM)) { // bought the premium upgrade! Log.d(TAG, "Purchase is premium upgrade. Congratulating user."); alert("Thank you for upgrading to premium!"); mIsPremium = true; updateUi(); setWaitScreen(false); } else if (purchase.getSku().equals(SKU_INFINITE_GAS)) { // bought the infinite gas subscription Log.d(TAG, "Infinite gas subscription purchased."); alert("Thank you for subscribing to infinite gas!"); mSubscribedToInfiniteGas = true; mTank = TANK_MAX; updateUi(); setWaitScreen(false); } } };
大体こんな流れである。当初アプリを再インストールした時の処理などは書かれていないんだろうと思い込んでたが、そんなことはなかった。
■購入情報の確認
アプリを再インストールしても購読した分がアプリに反映されなくてはいけない。
MainActivity.javaのmGotInventoryListenerの生成
mSubscribedToInfiniteGasに書込がされる部分は以下の部分である。
Purchase infiniteGasPurchase = inventory.getPurchase(SKU_INFINITE_GAS); mSubscribedToInfiniteGas = (infiniteGasPurchase != null && verifyDeveloperPayload(infiniteGasPurchase)); Log.d(TAG, "User " + (mSubscribedToInfiniteGas ? "HAS" : "DOES NOT HAVE") + " infinite gas subscription."); if (mSubscribedToInfiniteGas) mTank = TANK_MAX;
getPurchaseは以下のようになっている。購入アイテムのHashMapからskuをkeyにして値を返す。
public Purchase getPurchase(String sku) { return mPurchaseMap.get(sku); }
mPurchaseMapはaddPurchaseメソッドでセットされている。
void addPurchase(Purchase p) { mPurchaseMap.put(p.getSku(), p); }
このメソッドがコールされるのはIabHelperのqueryPurchases。getPurchasesでService側にownedItemsを問い合わせる。
Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(), itemType, continueToken);
取得したownedItemsからpurchaseDataListを取得しsignatureが一致しているものをinvに追加する。
ArrayList<String> ownedSkus = ownedItems.getStringArrayList( RESPONSE_INAPP_ITEM_LIST); ArrayList<String> purchaseDataList = ownedItems.getStringArrayList( RESPONSE_INAPP_PURCHASE_DATA_LIST); ArrayList<String> signatureList = ownedItems.getStringArrayList( RESPONSE_INAPP_SIGNATURE_LIST); for (int i = 0; i < purchaseDataList.size(); ++i) { String purchaseData = purchaseDataList.get(i); String signature = signatureList.get(i); String sku = ownedSkus.get(i); if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) { logDebug("Sku is owned: " + sku); Purchase purchase = new Purchase(itemType, purchaseData, signature); if (TextUtils.isEmpty(purchase.getToken())) { logWarn("BUG: empty/null token!"); logDebug("Purchase data: " + purchaseData); } // Record ownership and token inv.addPurchase(purchase); } else { logWarn("Purchase signature verification **FAILED**. Not adding item."); logDebug(" Purchase data: " + purchaseData); logDebug(" Signature: " + signature); verificationFailed = true; } }
購入アイテムのHashMapは生成された。もう少し辿るとqueryPurchasesはqueryInventoryメソッドからコールされる。queryInventoryは以下のqueryInventoryAsyncからコールされる。
public void queryInventoryAsync(final boolean querySkuDetails, final List<String> moreSkus, final QueryInventoryFinishedListener listener) { final Handler handler = new Handler(); checkSetupDone("queryInventory"); flagStartAsync("refresh inventory"); (new Thread(new Runnable() { public void run() { IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful."); Inventory inv = null; try { inv = queryInventory(querySkuDetails, moreSkus); } catch (IabException ex) { result = ex.getResult(); } flagEndAsync(); final IabResult result_f = result; final Inventory inv_f = inv; handler.post(new Runnable() { public void run() { listener.onQueryInventoryFinished(result_f, inv_f); } }); } })).start(); }
queryInventoryAsyncはonIabSetupFinishedの中でコールされ、onIabSetupFinished自体はサービスがConnectedした時にコールされる。
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() { public void onIabSetupFinished(IabResult result) { Log.d(TAG, "Setup finished."); if (!result.isSuccess()) { // Oh noes, there was a problem. complain("Problem setting up in-app billing: " + result); return; } // Hooray, IAB is fully set up. Now, let's get an inventory of stuff we own. Log.d(TAG, "Setup successful. Querying inventory."); mHelper.queryInventoryAsync(mGotInventoryListener); } });
という事でServiceがbindされた時にServiceに購入アイテムの問い合わせを要求する。そしてHashMapが完成してUIに反映される。
サンプルアプリのアップロード
必要情報を入力して公開できる状態にする必要がある。但し実際に公開せず下書きの状態にしておく。
サンプル商品情報の入力
必要情報を入力し公開状態にする必要がある。
全てのアプリケーション>アプリ>アプリ内アイテム
商品を登録する。日本円における最低料金は99円だが、各通貨ごとにも最低料金があるので全て満たす必要がある。
設定>アカウントの詳細
開発者のアカウントでは購入できないので、テスト用のアクセス権がある Gmail アカウントでテスト用のアカウントを設定する。
エラー
このバージョンのアプリは、Google Playを通じたお支払いはご利用になれません。詳しくはヘルプセンターをご覧ください。
アプリが署名されている必要がありadbで署名されたapkをインストールする必要がある。
adb -d install ./adb -d install ~/TrivialDrive/TrivialDrive.apk
出版社はこのアイテムを購入できません
Google Play の規約にもある通り開発者アカウントでは購入できない。