@blog.justoneplanet.info

日々勉強

TrivialDriveのコードを読んでみる

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

コメントはまだありません»

No comments yet.

RSS feed for comments on this post.TrackBack URL

Leave a comment