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 の規約にもある通り開発者アカウントでは購入できない。