2013年5月22日水曜日

Google I/O 2013 - Android : In-App Billing Version 3

In-App Billing Version 3



# スピーカーの人最初すごい早口だけど途中から普通になる。。。
  • V2 では purchase を作成する部分はまだ簡単だが、購入結果を処理したり、スリープ中にメッセージがきたりするのを処理するとコードがすごく複雑になる。
  • BroadcastReceiver は長い時間生きてはいられない。システムに殺されることがある。
  • BroadcastReceiver を処理する Service を用意したりして、さらに複雑になる。
  • 購入情報の呼び出しは expensive な API call なので、購入情報をローカル(データベースなど)に保存する必要もある。。。
  • ユーザーが購入情報を改ざんしないようにデータベースを暗号化する必要もある。。。


単純なケースは問題ない

ユーザーが購入 → item → アプリ


アプリがスリープ状態だと困ったことになる

ユーザーが購入 → item → receiver → アプリ

別のコンポーネント(receiver)がパッケージをピックアップしなければならず、そのコンポーネントはアプリケーションが起きたときに、ちゃんとアイテムを届けるように面倒をみなければならない

このコンポーネントがアプリケーションを見失ったときに問題が起こる

V2 ではユーザーの購入が適切な場所に行くように確認するため、開発者は一緒に処理を担う多くのコンポーネントを記述しなければならなかった


(V3のコンポーネントを紹介するところで、最初V3では7つのコンポーネントがあるっていってるけど、そこは冗談なので勘違いしないようにw)


V3 の Billing は同期型

Activity ⇔ Google Play
  • 同期型なので、アプリはレスポンスをすぐに受けとれる
  • V2 では restore purchase が時間のかかる処理だったので、クライアント側で情報を保持しておく必要があった
  • V3 では Google Play 内に購入のキャッシュを持つので、たくさん API を呼んでも大丈夫(例えばアプリの起動ごとに呼んでもOK)



V3 の実装について

1. デバイス及び Google Play が V3 をサポートしているかチェック public void onServiceConnected(Component name, IBinder service) { mService = IInAppBillingService.Stub.asInterface(service); int response = mService.isBillingSupported(3, getPackageName(), "inapp"); if(response == BILLING_RESPONSE_RESULT_OK) { // has billing! } else { // no billing V3... } } V3 は Froyo 以上 + Google Play v3.9.16 以上の様々なデバイスでサポートされており、数ヶ月まえで 90%以上 がサポートしていることを確認している


2. 購入済みのアイテムを取得する
getPurchases API を呼ぶ Bundle bundle = mService.getPurchases(3, mContext.getPackageName(), "inapp"); if(bundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) { ArrayList mySkus, myPurchases, mySignature; mySkus = bundle.getStringArrayList(RESPONSE_INAPP_ITEM_LIST); myPurchases = bundle.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST); mySignatures = bundle.getStringArrayList(RESPONSE_INAPP_PURCHASE_SIGNATURE_LIST); // handle items here }

3. 購入の流れを開始する

ユーザーが購入したにも関わらず、購入したことになっていないという状況を起こさないために、すべてのアイテムを managed items にすること Bundle bundle = mService.getBuyIntent(3, "com.example.myapp", MY_SKU, "inapp", developerPayload); PendingIntent pendingIntent = bundle.getParcelable(RESPONSE_BY_INTENT); if(bundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) { startIntentSenderForResult(pendingIntent.getIntentSender(), RC_BUY, new Intent(), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0)); } # スライドでは getIntentSender() が付いてなかったけどいると思う

4. 購入結果を受けとる public void onActivityResult(int requestCode, int resultCode, Intent data) { if(requestCode == RC_BUY) { int responseCode = data.getIntExtra(RESPONSE_CODE); String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA); String signature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE); // ... } }
Purchase data
{
  "orderId": ...,
  "packageName": ...,
  "productId": ...,
  "purchaseTime": ...,
  "purchaseState": ...,
  "developerPayload": ...,
  "purchaseToken": ...
}
購入フローでアプリがクラッシュして結果を onActivityResult() で受けとれなくても、次回起動時に getPurchases() を呼べば購入したかどうかがわかる

一度購入したものは、デバイスが変わったり一度アプリをアンインストールしてもなくならないが、永続化したくない購入アイテムもある(ゲームのコインやシーズンパスなど)。これらは V2 では unmanaged item として使われていた。

V3 では unmanaged item はない。V3 では Consumption API を使う。consume は purchase の反対。consume すると同じアイテムをまた購入できるようになる。 Bundle b = mService.consumePurchase( 3, // API version "com.example.xyz", // package name token // purchase token ); いつ consume すべきか?
  • 1. アイテムが実際に使われたとき(Google Play がユーザーの在荷を管理)
  • 2. 購入後すぐ、そして startup 時にも(アプリがユーザーの在荷を管理)

1 は同じアイテムを使い終わる前に再度購入したい場合には使えない、その場合は 2 にする必要があるが、ユーザーの在荷をちゃんと管理する必要がある。購入後 consume する前にアプリがクラッシュすると同じアイテムを買えなくなるので startup 時にも行う。


2 の方法のまとめ
  • on startup:
    getPurchases()
    もし potion があるなら consume()

  • ユーザーが購入したいとき:
    getBuyIntent() して Intent を起動

  • onActivityResult:
    購入が成功したら consume()

  • consume():
    consume が成功したらアプリ内の在荷として potion を追加
    ここで追加する理由は、購入状態は複数のデバイスで共有されるので、複数のデバイスで consume しても1つのデバイスの在荷にだけ追加されるようにするため



アイテムの詳細を取得する

コードからタイトル、詳細、価格を取得する
Bundle getSkuDetails(
  int apiVersion,
  String packageName,
  String type,
  Bundle skusBundle
);

戻り値の bundle の例
{
  "productId": "xyz123"
  "type": "inapp" | "subs"
  "price": "$1.99"
  "title": "100 coins"
  "description": "..."
}



Subscriptions

V3 ではより簡単に、アイテム課金と同じ感じになった

1. Subscriptions に対応しているかチェックする public void onServiceConnected(Component name, IBinder service) { mService = IInAppBillingService.Stub.asInterface(service); int response = mService.isBillingSupported(3, getPackageName(), "subs"); if(response == BILLING_RESPONSE_RESULT_OK) { // has billing! } else { // no billing V3... } } isBillingSupported() の第3引数を "subs" にする


2. 購入の流れを開始する Bundle bundle = mService.getBuyIntent(3, "com.example.myapp", MY_SKU, "subs", developerPayload); PendingIntent pendingIntent = bundle.getParcelable(RESPONSE_BY_INTENT); if(bundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) { startIntentSenderForResult(pendingIntent.getIntentSender(), RC_BUY, new Intent(), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0)); } # スライドでは getIntentSender() が付いてなかったけどいると思う

getBuyIntent() の第4引数を "subs" にする

subscription は consume できない!


3. Subscription が Active か調べる Bundle bundle = mService.getPurchases(3, mContext.getPackageName(), "subs"); if(bundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) { // item と同じ } getPurchases() の第3引数を "subs" にする

subscription の状態には trial, active, cancelled, expired があるが、getPurchases() では trial、active、期限の残っている cancelled のものが返ってくる

Google Play アプリは 24時間ごとに Refresh するので getPurchases() のキャッシュもそのタイミングでリフレッシュする


UI スレッドをブロックしないように気をつける

Safe ro UI thread
  • isBillingSupported
  • getBuyIntent

Don't call on UI thread
  • getPurchases
  • consumePurchase
  • getSkuDetails
TrivalDrive というサンプルがある。これには非同期のラッパーが用意されている


Server side subscription API

subscription をキャンセルする
GET https://www.googleapis.com/androidpublisher/v1/applications/{packageName}/subscriptions/{subscriptionId}/purchases/{token}

{subscriptionId} は SKU, {token} は getPurchases() で取得できるもの

response の例
{
  "kind": "androidpublisher#subscriptionPurchase",
  "initiationTimestampMsec": ...,
  "validUntilTimestampMsec": ...,
  "autoRenewing": ...
}
More at : https://developer.google.com/android-publisher/v1/


Security

海賊対策

正当な購入かどうか確かめる

Risk Model
  • audience
  • item value
  • total legitimate purchases
  • technical difficulty
  • total purchases
  • observed # of fake purchases
  • likelihood of piracy
海賊行為を完全になくす方法というのはないが、海賊行為をしにくくさせるために以下を利用する
1. developer payload
2. signature verification
3. server-side validation


1. developer payload

任意の文字列のタグ
アイテムの所有者を確かめるために利用したり
(アイテムを購入済みの人のデータベースをダンプして別のユーザーのデバイスにいれた場合など、developer payload にユーザー固有の ID を入れておけばチェックできる)


2. Signature Verification

アプリは public key を持っていて、Google Play は 署名した purchases を返すのでいつも signature をチェックする!
(別のアプリが Google Play のふりをして偽の購入情報を渡してくる(Man in the middle attack)のを防ぐ)

Signature Verification のヘルパークラスが TrivalDrive サンプルにある


3. Server-Side Validation

signature をチェックし(偽のクライアントアプリから送られてないかチェック)、order number(orderId のことだと思われる) をチェックし(サーバーは全てのユーザーからくる全ての単一の購入を毎回見ており、全ての購入は一意な order number と一緒に送られてくるので、同じ number での2回目の購入があったら、それはフェイクだとわかる)、secure な handshake を行う(validated or not の boolean を返す REST call では、簡単に man in the middle attack で回避できてしまう)
(デバイスがハックされて framework が compromise された場合を防ぐ)


Sandbox

以前は mock product, mock purchase flow, mock result
real product でテストするには実際にお金がかかった

今は real product, real credit card, real purchase flow, real result, no charge!

unpublish なアプリで購入しようとすると
"This is a tes purchase, you will not be charged."
と表示された購入画面になる


Sample:TrivalDrive
  • SDK manager にある、または code.google.com/p/marketbilling
  • google code のほうが SDK manager のものより up-to-date なのでおすすめ




1 件のコメント: