2013年2月19日火曜日

Android Subscriptions(API Version3)の実装

2/1 に Android アプリ内課金(API Version3)の実装 を書いたときにはまだ API Version 3 は Subscriptions(定期課金) に対応していなかったのですが、2/14 から対応しました。



version 3 の API の基本は上記のエントリ及び Implementing In-app Billing (IAB Version 3) を見てください。
version 3 の subscriptions の購入フローはプロダクト購入フローとほぼ同じなので、上記のエントリを先にみることをオススメします。

Implementing Subscriptions

Subscription の購入フローもプロダクト購入フローとほぼ同じで、違う点は product type が "subs" である点です。購入結果は Activity の onActivityResult メソッドで受けとります(in-app products と同じ)。 Bundle bundle = mService.getBuyIntent(3, "com.example.myapp", MY_SKU, "subs", developerPayload); PendingIntent pendingIntent = bundle.getParcelable(RESPONSE_BUY_INTENT); if (bundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) { // Start purchase flow (this brings up the Google Play UI). // Result will be delivered through onActivityResult(). startIntentSenderForResult(pendingIntent, RC_BUY, new Intent(), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0)); } 有効な subscriptions を問い合わせるには、product type に "subs" を指定して getPurchases メソッドを使います。 Bundle activeSubs = mService.getPurchases(3, "com.example.myapp", "subs", continueToken); 戻り値の Bundle はユーザーがオーナーの全ての有効な subscriptions(期限が切れていない subscriptions)を返します。更新せずに subscription の期限が切れると、戻り値の Bundle には含まれなくなります。


2013年2月14日木曜日

Android AutoCompleteTextView で候補に入力履歴を表示する

AutoCompleteTextView はドロップダウンで入力候補を表示してくれる EditText です。
(EditText を extends してるのに AutoCompleteEditText じゃないのはなぜなんだ)

こんな感じで候補の一覧を Adapter として用意して AutoCompleteTextView の setAdapter() でセットします。 public class CountriesActivity extends Activity { protected void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.countries); AutoCompleteTextView textView = (AutoCompleteTextView) findViewById(R.id.countries_list); ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line, COUNTRIES); textView.setAdapter(adapter); } private static final String[] COUNTRIES = new String[] { "Belgium", "France", "Italy", "Germany", "Spain" }; } 過去の入力を候補をして表示したい場合、候補が随時かわるので上記の方法は使えません。

そこで、SearchManager の出番です。
SearchManager については以前のエントリ「Y.A.M の雑記帳 - Android SearchManager 検索ボックスを使うぜ!」あたりをみてください。

簡単にいうと、SearchManager から候補一覧の Cursor(候補は ContentProvider として提供される)を取得して、それを Adapter にセットすればいいわけです。

SearchManager から候補一覧を取得するには SearchManager の getSuggestions() を呼べばいいのですが、残念ながら @hide です。 ただ、SearchableInfo があれば同じことはできます。

シンプルな Adapter だとだいたいこんな感じです。 import android.app.SearchManager; import android.app.SearchableInfo; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.support.v4.widget.ResourceCursorAdapter; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; class SearchRecentSuggestionsAdapter extends ResourceCursorAdapter { private static final boolean DEBUG = false; private static final String LOG_TAG = "SearchRecentSuggestionsAdapter"; private static final int QUERY_LIMIT = 50; private SearchableInfo mSearchable; private boolean mClosed = false; static final int INVALID_INDEX = -1; private int mText1Col = INVALID_INDEX; public SearchRecentSuggestionsAdapter(Context context, SearchableInfo searchable) { super(context, R.layout.search_recent_row, null, true); mSearchable = searchable; } /** * Overridden to always return false, since we cannot be sure * that suggestion sources return stable IDs. */ @Override public boolean hasStableIds() { return false; } /** * Use the search suggestions provider to obtain a live cursor. This will be * called in a worker thread, so it's OK if the query is slow (e.g. round * trip for suggestions). The results will be processed in the UI thread and * changeCursor() will be called. */ @Override public Cursor runQueryOnBackgroundThread(CharSequence constraint) { if (DEBUG) Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")"); String query = (constraint == null) ? "" : constraint.toString(); Cursor cursor = null; try { cursor = getSuggestions(mSearchable, query, QUERY_LIMIT); return cursor; } catch (RuntimeException e) { Log.w(LOG_TAG, "Search suggestions query threw an exception.", e); } return null; } public Cursor getSuggestions(SearchableInfo searchable, String query, int limit) { if (searchable == null) { return null; } String authority = searchable.getSuggestAuthority(); if (authority == null) { return null; } Uri.Builder uriBuilder = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority); // if content path provided, insert it now final String contentPath = searchable.getSuggestPath(); if (contentPath != null) { uriBuilder.appendEncodedPath(contentPath); } // append standard suggestion query path uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY); // get the query selection, may be null String selection = searchable.getSuggestSelection(); // inject query, either as selection args or inline String[] selArgs = null; if (selection != null) { // use selection if provided selArgs = new String[] { query }; } else { // no selection, use REST pattern uriBuilder.appendPath(query); } if (limit > 0) { uriBuilder.appendQueryParameter(SearchManager.SUGGEST_PARAMETER_LIMIT, String.valueOf(limit)); } Uri uri = uriBuilder.build(); return mContext.getContentResolver().query(uri, null, selection, selArgs, null); } public void close() { if (DEBUG) Log.d(LOG_TAG, "close()"); changeCursor(null); mClosed = true; } /** * Cache columns. */ @Override public void changeCursor(Cursor c) { if (DEBUG) Log.d(LOG_TAG, "changeCursor(" + c + ")"); if (mClosed) { Log.w(LOG_TAG, "Tried to change cursor after adapter was closed."); if (c != null) c.close(); return; } try { super.changeCursor(c); if (c != null) { mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1); } } catch (Exception e) { Log.e(LOG_TAG, "error changing cursor and caching columns", e); } } /** * Tags the view with cached child view look-ups. */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { View v = super.newView(context, cursor, parent); v.setTag(new ViewHolder(v)); return v; } /** * Cache of the child views of drop-drown list items, to avoid looking up * the children each time the contents of a list item are changed. */ private final static class ViewHolder { public final TextView mText1; public ViewHolder(View v) { mText1 = (TextView) v.findViewById(android.R.id.text1); } } @Override public void bindView(View view, Context context, Cursor cursor) { ViewHolder views = (ViewHolder) view.getTag(); if (views.mText1 != null) { String text1 = getStringOrNull(cursor, mText1Col); views.mText1.setText(text1); views.mText1.setVisibility(TextUtils.isEmpty(text1) ? View.GONE : View.VISIBLE); } } /** * Gets the text to show in the query field when a suggestion is selected. * * @param cursor * The Cursor to read the suggestion data from. The Cursor should * already be moved to the suggestion that is to be read from. * @return The text to show, or null if the query should not be * changed when selecting this suggestion. */ @Override public CharSequence convertToString(Cursor cursor) { if (cursor == null) { return null; } String query = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_QUERY); if (query != null) { return query; } if (mSearchable.shouldRewriteQueryFromData()) { String data = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_DATA); if (data != null) { return data; } } if (mSearchable.shouldRewriteQueryFromText()) { String text1 = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_TEXT_1); if (text1 != null) { return text1; } } return null; } /** * This method is overridden purely to provide a bit of protection against * flaky content providers. * * @see android.widget.ListAdapter#getView(int, View, ViewGroup) */ @Override public View getView(int position, View convertView, ViewGroup parent) { try { return super.getView(position, convertView, parent); } catch (RuntimeException e) { Log.w(LOG_TAG, "Search suggestions cursor threw exception.", e); // Put exception string in item title View v = newView(mContext, mCursor, parent); if (v != null) { ViewHolder views = (ViewHolder) v.getTag(); TextView tv = views.mText1; tv.setText(e.toString()); } return v; } } public static String getColumnString(Cursor cursor, String columnName) { int col = cursor.getColumnIndex(columnName); return getStringOrNull(cursor, col); } private static String getStringOrNull(Cursor cursor, int col) { if (col == INVALID_INDEX) { return null; } try { return cursor.getString(col); } catch (Exception e) { Log.e(LOG_TAG, "unexpected error retrieving valid column from cursor, " + "did the remote process die?", e); return null; } } } search_recent_row.xml <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:padding="8dip" android:layout_width="match_parent" android:layout_height="48dip" > <TextView android:id="@android:id/text1" style="?android:attr/dropDownItemStyle" android:textAppearance="?android:attr/textAppearanceSearchResultTitle" android:singleLine="true" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerVertical="true" /> </RelativeLayout> この Adapter を AutoCompleteTextView にセットすれば OK です。 public class InputActivity extends Activity { protected void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); final AutoCompleteTextView textView = (AutoCompleteTextView) findViewById(R.id.editText1); SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); final SearchableInfo info = searchManager.getSearchableInfo(getComponentName()); textView.setThreshold(info.getSuggestThreshold()); textView.setImeOptions(info.getImeOptions()); int inputType = info.getInputType(); if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) { inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; if (info.getSuggestAuthority() != null) { inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; } } textView.setInputType(inputType); if (info.getSuggestAuthority() != null) { // 候補の CursorAdapter をセット SearchRecentSuggestionsAdapter adapter = new SearchRecentSuggestionsAdapter(this, info); textView.setAdapter(adapter); } findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 検索文字列を候補に追加 String query = textView.getText().toString(); SearchRecentSuggestions suggestions = new SearchRecentSuggestions(InputActivity.this, info.getSuggestAuthority(), SearchRecentSuggestionsProvider.DATABASE_MODE_QUERIES); suggestions.saveRecentQuery(query, null); } }); } } この Activity にはもちろん AndroidManifest.xml で searchable をセットしておくことが必要です。




2013年2月5日火曜日

-keep でもコードから呼ばれてないメソッドは削除されるっぽい

proguard の話です。

Hoge というクラスのインスタンスを完全修飾名(文字列)から ClassLoader を使って動的に生成しているのですが、 -keep class com.example.android.Hoge だけだと getConstructor() を呼んだときに NoSuchMethodException が起こってしまいます。

getConstructor() で取得しようとしているコンストラクタは引数ありで、コードからは呼ばれていません。

そのためなのか、どうも proguard が削除してしまっているよう。

そこで -keep class com.example.android.Hoge { public <init>(com.example.android.Fuga); } のようにすれば削除されなくなります。

ちなみに、com.example.android 内の全てのクラスに適用したい場合は -keep class com.example.android.* でいいですが、サブパッケージ内のクラスにも適用したい場合は -keep class com.example.android.** にします。


2013年2月1日金曜日

Android アプリ内課金(API Version3)の実装

Implementing In-app Billing (IAB Version 3)
  • 1. プロジェクトに In-app Billing ライブラリを追加する
  • 2. AndroidManifest.xml を変更する
  • 3. ServiceConnection を作成し、IInAppBillingService にバインドする
  • 4. アプリから In-app Billing リクエストを IInAppBillingService に送る
  • 5. Google Play からの In-app Billing レスポンスを処理する


1. プロジェクトに In-app Billing ライブラリを追加する

TrivialDriva サンプルから IInAppBillingService.aidl ファイルを Android プロジェクトにコピーする。Eclipse なら /src ディレクトリにインポートする。

/gen ディレクトリ内に IInAppBillingService.java が作成されていることを確認する。

* サンプルの取得
1. Android SDK Manager で Google Play Billing Library をインストールする
2. [sdk-install-dir]/extras/google/play_billing/in-app-billing-v03/samples/TrivialDrive/ からプロジェクトをインポートする



2. AndroidManifest.xml を変更する <uses-permission android:name="com.android.vending.BILLING" /> を追加する



3. ServiceConnection を作成する
----- アプリ ---------------------
|                               |
| メイン機能 - ServiceConnection ---- Google Play
|                               |
---------------------------------

少なくとも以下の処理がアプリ内で必要
・IInAppBillingService にバインドする
・Google Play アプリに IPC で billing リクエストを送る
・各 billing リクエストごとに返ってくる同期レスポンスメッセージを処理する


・IInAppBillingService にバインドする

Google Play の In-app Billing サービスへの接続を確立するには、Activity を IInAppBillingService にバインドするための ServiceConnection を実装する。
onServiceDisconnectedonServiceConnected メソッドを Override し、接続が確立された後に IInAppBillingService インスタンスへの参照を取得する。
IInAppBillingService mService; ServiceConnection mServiceConn = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { mService = null; } @Override public void onServiceConnected(ComponentName name, IBinder service) { mService = IInAppBillingService.Stub.asInterface(service); } };

Activity の onCreate メソッド内で、bindService メソッドを呼んでバインドする。メソッドには In-app Billing サービスを参照する Intent および ServiceConnection のインスタンスを渡す。
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); bindService(new Intent("com.android.vending.billing.InAppBillingService.BIND"), mServiceConn, Context.BIND_AUTO_CREATE);

Activity が終了する場合、In-app Billing サービスからのアンバインドを忘れずに行う。
@Override public void onDestroy() { super.onDestroy(); if (mServiceConn != null) { unbindService(mServiceConn); } }

4. In-app Billing リクエストを作成する

アプリが Google Play に接続されたら、アプリ内課金アイテムのリクエストを開始することができる。 Google Play が支払用のインタフェースを作成するため、アプリが直接支払い取引を処理する必要はない。
アイテムが購入されると、Google Play はユーザーがそのアイテムの所有権を取得したと認識し、そのアイテムが消費されるまで同じプロダクトIDのアイテムが購入されるのを防止する。
アプリではアイテムがどのように消費されるかをコントロールすることができ、Google Play にそのアイテムが再度購入できるようになったことを通知できる。
また、ユーザーによって作成された購入リストを Google Play から素早く取得することができる。これは、例えば、ユーザーがアプリを起動したときにユーザーの購入リストをリストアしたときに便利。


・購入可能なアイテムを問い合わせる

In-app Billing Version 3 API を使って Google Play からアイテムの詳細を問い合わせることができる。In-app Billing サービスにリクエストを送るには、プロダクトIDの String ArrayList を作成し、それを "ITEM_ID_LIST" というキーで Bundle に保持する。
ArrayList<String> skuList = new ArrayList<String>(); skuList.add("premiumUpgrade"); skuList.add("gas"); Bundle querySkus = new Bundle(); querySkus.putStringArrayList("ITEM_ID_LIST", skuList);

Google Play から情報を取得するには、In-app Billing Version 3 API の getSkuDetails メソッドを呼び出す。第1引数には In-app Billing API version の "3"、第2引数には呼び出しアプリのパッケージ名、第3引数には購入タイプの "inapp"、第4引数には作成した Bundle を渡す。
Bundle skuDetails = mService.getSkuDetails(3, getPackageName(), "inapp", querySkus);

リクエストが成功した場合、返ってきた Bundle には RESPONSE_CODE というキーで BILLING_RESPONSE_RESULT_OK (0) が含まれる。
その他の Response Code には以下のものがある
  • BILLING_RESPONSE_RESULT_OK 0
    Success

  • BILLING_RESPONSE_RESULT_USER_CANCELED 1
    User pressed back or canceled a dialog

  • BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE 3
    Billing API version is not supported for the type requested

  • BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE 4
    Requested product is not available for purchase

  • BILLING_RESPONSE_RESULT_DEVELOPER_ERROR 5
    Invalid arguments provided to the API. This error can also indicate that the application was not correctly signed or properly set up for In-app Billing in Google Play, or does not have the necessary permissions in its manifest

  • BILLING_RESPONSE_RESULT_ERROR 6
    Fatal error during the API action

  • BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED 7
    Failure to purchase since item is already owned

  • BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED 8
    Failure to consume since item is not owned


getSkuDetails メソッドをメインスレッドから呼ばないこと。このメソッドはネットワークリクエストのトリガーになり、メインスレッドをブロックする。

問い合わせた結果は、"DETAIL_LIST" というキーで String ArrayList に格納され、各購入情報は JSON 形式の文字列で格納される。
int response = skuDetails.getInt("RESPONSE_CODE"); if (response == 0) { ArrayList<String> responseList = skuDetails.getStringArrayList("DETAILS_LIST"); for (String thisResponse : responseList) { JSONObject object = new JSONObject(thisResponse); String sku = object.getString("productId"); String price = object.getString("price"); if (sku.equals("premiumUpgrade")) mPremiumUpgradePrice = price; else if (sku.equals("gas")) mGasPrice = price; } } JSON fields には以下のキーがある
  • productId
    The product ID for the product.

  • type
    Value must be “inapp” for an in-app purchase type.

  • price
    Formatted price of the item, including its currency sign. The price does not include tax.

  • title
    Title of the product.

  • description
    Description of the product.


・アイテムを購入する

アプリから購入リクエストを開始するには、In-app Billing Service の getBuyIntent メソッドを呼び出す。 第1引数には In-app Billing API version の "3"、第2引数には呼び出しアプリのパッケージ名、第3引数には product ID、第4引数には購入タイプの "inapp"、第4引数には developerPayload 文字列を渡す。 developerPayload 文字列は、購入情報として Google Play から返してほしい付加的な引数を指定するのに使う。
Bundle buyIntentBundle = mService.getBuyIntent(3, getPackageName(), sku, "inapp", "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ");

リクエストが成功した場合、返ってきた Bundle には RESPONSE_CODE というキーで BILLING_RESPONSE_RESULT_OK (0) が含まれる。また、"BUY_INTENT" というキーで取得できる PendingIntent で購入フローを開始できる。
PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");

購入処理を完了するには、取得した pendingIntent で startIntentSenderForResult メソッドを呼び出す。
startIntentSenderForResult(pendingIntent.getIntentSender(), 1001, new Intent(), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0));

Google Play は PendingIntent のレスポンスをアプリの onActivityResult に返す。 onActivityResult メソッドは Activity.RESULT_OK (1) もしくは Activity.RESULT_CANCELED (0) を resultCode として持つ。
  • RESPONSE_CODE
    0 if the purchase was success, error otherwise.

  • INAPP_PURCHASE_DATA
    A String in JSON format that contains details about the purchase order. See table 4 for a description of the JSON fields.

  • INAPP_DATA_SIGNATURE
    String containing the signature of the purchase data that was signed with the private key of the developer.


購入データは JSON 形式の文字列として Intent に格納されており、"INAPP_PURCHASE_DATA" というキーで取得できる。

例えば、次のようなデータが格納されている
'{ "orderId":"12999763169054705758.1371079406387615", "packageName":"com.example.app", "productId":"exampleSku", "purchaseTime":1345678900000, "purchaseState":0, "developerPayload":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ", "purchaseToken":"rojeslcdyyiapnqcynkjyyjh" }'
  • orderId
    A unique order identifier for the transaction. This corresponds to the Google Wallet Order ID.

  • packageName
    The application package from which the purchase originated.

  • productId
    The item's product identifier. Every item has a product ID, which you must specify in the application's product list on the Google Play publisher site.

  • purchaseTime
    The time the product was purchased, in milliseconds since the epoch (Jan 1, 1970).

  • purchaseState
    The purchase state of the order. Possible values are 0 (purchased), 1 (canceled), or 2 (refunded).

  • developerPayload
    A developer-specified string that contains supplemental information about an order. You can specify a value for this field when you make a getBuyIntent request.

  • purchaseToken
    A token that uniquely identifies a purchase for a given item and user pair.


@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == 1001) { int responseCode = data.getIntExtra("RESPONSE_CODE", 0); String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA"); String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE"); if (resultCode == RESULT_OK) { try { JSONObject jo = new JSONObject(purchaseData); String sku = jo.getString("productId"); alert("You have bought the " + sku + ". Excellent choice, adventurer!"); } catch (JSONException e) { alert("Failed to parse purchase data."); e.printStackTrace(); } } } } Security Recommendation : 購入リクエストを送るときに、この購入リクエストをユニークに識別する文字列トークンを作成し、developerPayload に含ませる。ランダムに生成した文字列をこのトークンに利用できる。Google Play からレスポンスを受けとった際に、orderId および developerPayload の文字列をチェックする。

セキュリティをさらに追加するには、自身のセキュアサーバー上でもチェックする。orderId がユニークな値で、以前に処理したことがないことを確認し、developerPayload の文字列が以前に送った購入リクエストのものと一致するかチェックする。


・購入したアイテムを問い合わせる

ユーザーによる購入情報を取得するには、In-app Billing Version 3 サービスの getPurchases メソッドを呼び出す。第1引数には In-app Billing API version の "3"、第2引数には呼び出しアプリのパッケージ名、第3引数には購入タイプの "inapp" を渡す。
Bundle ownedItems = mService.getPurchases(3, getPackageName(), "inapp", null);

Google Play サービスは、デバイスに現在ログインしているユーザーアカウントの購入についてのみ返す。 リクエストが成功した場合、返ってきた Bundle には RESPONSE_CODE というキーで BILLING_RESPONSE_RESULT_OK (0) が含まれる。また、"INAPP_PURCHASE_ITEM_LIST" というキーで product ID のリストが、"INAPP_PURCHASE_DATA_LIST" というキーでオーダー詳細のリストが、"INAPP_DATA_SIGNATURE" というキーで各購入のシグネチャのリストが含まれる。

パフォーマンス改善のために、getPurchase が最初に呼ばれたとき、In-app Billing サービスは700個のプロダクトまでしか返さない。
ユーザーが大量のプロダクトを持っている場合、Google Play は "INAPP_CONTINUATION_TOKEN" というキーに文字トークンを割り当て、さらに取得できるプロダクトがあることを示す。引数としてこのトークンを渡す事でアプリは続きの getPurcases を呼び出す事ができる。Google Play はユーザーのすべてのプロダクトがアプリに送られるまでレスポンスの Bundle に続きのトークンを含めて返す。
nt response = ownedItems.getInt("RESPONSE_CODE"); if (response == 0) { ArrayList ownedSkus = ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST"); ArrayList purchaseDataList = ownedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST"); ArrayList signatureList = ownedItems.getStringArrayList("INAPP_DATA_SIGNATURE"); String continuationToken = ownedItems.getString("INAPP_CONTINUATION_TOKEN"); for (int i = 0; i < purchaseDataList.size(); ++i) { String purchaseData = purchaseDataList.get(i); String signature = signatureList.get(i); String sku = ownedSkus.get(i); // do something with this purchase information // e.g. display the updated list of products owned by user } // if continuationToken != null, call getPurchases again // and pass in the token to retrieve more items }
  • RESPONSE_CODE
    0 if the request was successful, error otherwise.

  • INAPP_PURCHASE_ITEM_LIST
    StringArrayList containing the list of productIds of purchases from this app.

  • INAPP_PURCHASE_DATA_LIST
    StringArrayList containing the details for purchases from this app. See table 4 for the list of detail information stored in each INAPP_PURCHASE_DATA item in the list.

  • INAPP_DATA_SIGNATURE_LIST
    StringArrayList containing the signatures of purchases from this app.

  • INAPP_CONTINUATION_TOKEN
    String containing a continuation token to retrieve the next set of in-app products owned by the user. This is only set by the Google Play service if the number of products owned by the user is very large. When a continuation token is present in the response, you must make another call to getPurchases and pass in the continuation token that you received. The subsequent getPurchases call returns more purchases and possibly another continuation token.


・購入アイテムの消費

In-app Billing Version 3 API を使って、Google Play の購入されたアイテムの所有権をトラックできる。一度アイテムが購入されると、その所有権があると考えられ Google Play から購入できなくなる。 Google Play が再びアイテムを購入可能にする前に、アイテムの消費リクエストを送らなければならない。
全ての managed な in-app products は消費可能。
消費メカニズムをどう利用するかは開発者次第。
典型的には、ユーザーが複数回購入したいような一時的な利益があるプロダクト(例えばゲーム内通貨や装備)は消費可能な実装にする。一度だけ購入したり、永続的な効果を提供するプロダクト(例えばプレミアムアップグレード)は消費しないように実装する。

購入アイテムの消費を記録するには、In-app Billing service の consumePurchase メソッドを呼び出す。第1引数には In-app Billing API version の "3"、第2引数には呼び出しアプリのパッケージ名、第3引数には purchaseToken 文字列を渡す。purchaseToken は成功した購入リクエストで Google Play サービスから返される "INAPP_PURCHASE_DATA" 文字列の一部として含まれる。
int response = mService.consumePurchase(3, getPackageName(), token);

Warinig: consumePurchase はメインスレッドから呼び出さない事。このメソッドはネットワークリクエストのトリガーになり、メインスレッドをブロックする。

購入した in-app product がユーザーにどのように提供されるかをコントロールしトラックするかは開発者の責任である。例えば、ゲーム内通貨をユーザーが購入した場合、購入した通貨の量に応じてプレイヤーの状態を変えなければならない。

Security Recommendation: ユーザーにアプリ内課金を消費する利益をプロビジョニングする前に消費リクエストを送らなければならない。アイテムをプロビジョンする前に Google Play から成功した消費レスポンスを受けとっていることを確認する。



---------------------

Version 2 に比べてかなりシンプルになっています。

サンプルアプリのコードをを見るのが一番いいです。

まだ Subscription は Version 3 で対応していない(coming soon とは書いてある)ので、早くきてほしい!