備忘録

備忘録

Android アプリ内課金を実装する

f:id:kagasu:20161218112052j:plain

Ⅰ. はじめに

「課金してアプリ内に表示される広告を消す」
「課金してアプリ内の石を購入する」
などの実装方法です。

継続課金(定期購入)については具体的に触れませんが、
この記事を一通り行った後公式ドキュメントを見れば簡単に実装できます。

備忘録として最低限をメモします。
安全なアプリを作るために公式ドキュメントも必ず参照してください。
https://developer.android.com/google/play/billing/billing_integrate.html?hl=ja

Ⅱ. 必要なもの

1. AndroidStudio

Androidアプリ開発に必要です

2. Googleアカウント2つ

1つ目はGoogle Play開発者アカウントとしてGoogle Play Developer Consoleに登録します。
2つ目は課金のテストを行う為のアカウントです。
残念ながらアカウント1つでは出来ません。

3. 25 USD

Google Play Developer Console への登録料として 25USD 必要です

Ⅲ. 実装方法

1. AndroidManifest.xml にアプリ内課金の権限を追加する

<uses-permission android:name="com.android.vending.BILLING" />

2. IInAppBillingService.aidl をプロジェクトに追加する

(android sdk)/extras/google/play_billing/IInAppBillingService.aidl ※1 を
プロジェクトのapp/aidl/com/android/vending/billing/IInAppBillingService.aidl ※2 にコピーします
f:id:kagasu:20161218110020p:plain

※1
デフォルトで C:\Users\%username%\AppData\Local\Android\sdk\extras\google\play_billing\IInAppBillingService.aidl にあります
※2
(android project name)\app\src\main\aidl\com\android\vending\billing\IInAppBillingService.aidl にコピーします

3. 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);
   }
};

4. 課金トランザクションの安全性を確保する

公式ドキュメントの通りです

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  Intent serviceIntent =
      new Intent("com.android.vending.billing.InAppBillingService.BIND");
  serviceIntent.setPackage("com.android.vending");
  bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
}

5. Activity 終了時にアプリ内課金サービスのバインドを解除する

公式ドキュメントの通りです
解除しないと端末のパフォーマンス低下につながります

@Override
public void onDestroy() {
    super.onDestroy();
    if (mService != null) {
        unbindService(mServiceConn);
    }
}

6. 簡単に Activity のレイアウトを作る

f:id:kagasu:20161218113921j:plain

ボタン名 機能
BUY アプリ内課金を実行するボタン
CHECK アプリ内課金の状態を取得するボタン
USE アプリ内課金で購入したアイテムを消費※3する

※3 消費とは
同じ課金アイテムを重複して買うことは出来ません。
同じ課金アイテムを重複して買わせるためには、
購入したものを消費する必要があります。
継続課金(定期購入)は消費できません。

7. ボタンを変数に割り当てる

onCreate
button1 = (Button)findViewById(R.id.button);
button2 = (Button)findViewById(R.id.button2);
button3 = (Button)findViewById(R.id.button3);

8. 購入ボタン(BUY)を実装する

onCreate
button1.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    buy();
  }
});
buy

RESPONSE_CODE一覧は以下のサイトに書かれています。
必要に応じて適切に実装して下さい。
https://developer.android.com/google/play/billing/billing_reference.html?hl=ja#billing-codes

private void buy() {
  try {
   // 購入リクエストの送信
   // item001 はGoogle Play Developer Consoleで作成した値を使う
   Bundle buyIntentBundle = billingService.getBuyIntent(3, getPackageName(), "item001", "inapp", "hoge");
   // レスポンスコードを取得する
   int response = buyIntentBundle.getInt("RESPONSE_CODE");
   // 購入可能
   // BILLING_RESPONSE_RESULT_OK
   if(response == 0) {
    // 購入フローを開始する
    PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");
    // 購入トランザクションの完了
    startIntentSenderForResult(
      pendingIntent.getIntentSender(),
      1001,
      new Intent(),
      Integer.valueOf(0),
      Integer.valueOf(0),
      Integer.valueOf(0));
   }
   // BILLING_RESPONSE_RESULT_USER_CANCELED
   else if(response == 1) {
    alert("購入がキャンセルされた");
   }
   // BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED
   else if(response == 7){
    alert("既に同じものを購入している");
   }
  } catch(Exception e) {
   e.printStackTrace();
   alert("buy unsuccessfully finished.");
  }
}
onActivityResult

課金の結果を取得する

@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");

  if (resultCode == RESULT_OK) {
   try {
    JSONObject jo = new JSONObject(purchaseData);
    String productId = jo.getString("productId");

    alert("購入成功しました");
    // 購入成功後すぐに消費する
    // use();
   }
   catch (JSONException e) {
    alert("Failed to parse purchase data.");
    e.printStackTrace();
   }
  } else {
   alert("課金に失敗しました");
  }
 }
}

8. 消費ボタン(USE)を実装する

onCreate
button2.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    use();
  }
});
use
private void use() {
 try {
  // 購入したものを全て消費する
  Bundle ownedItems = billingService.getPurchases(3, getPackageName(), "inapp", null);

  int response = ownedItems.getInt("RESPONSE_CODE");
  if (response == 0) {
   ArrayList<String> ownedSkus = ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
   ArrayList<String> purchaseDataList = ownedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
   ArrayList<String> signatureList = ownedItems.getStringArrayList("INAPP_DATA_SIGNATURE_LIST");
   String continuationToken = ownedItems.getString("INAPP_CONTINUATION_TOKEN");

   for (int i = 0; i < purchaseDataList.size(); ++i) {
    String purchaseData = purchaseDataList.get(i);
    JSONObject object = new JSONObject(purchaseData);
    String productId = object.getString("productId");
    String purchaseToken = object.getString("purchaseToken");

    // 消費する
    response = billingService.consumePurchase(3, getPackageName(), purchaseToken);

    // 正常終了
    if(response == 0) {
     alert(productId + "を消費しました。");
    } else {
     alert(purchaseData);
    }
   }
  }
 }catch(Exception e) {
  e.printStackTrace();
 }
}

9. アプリ内課金の状態を取得するボタン(CHECK)を実装する

先程の use 関数から消費処理を消すだけです。

onCreate
button3.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    check();
  }
});
check
private void check() {
 try {
  // 購入したものを確認する
  Bundle ownedItems = billingService.getPurchases(3, getPackageName(), "inapp", null);

  int response = ownedItems.getInt("RESPONSE_CODE");
  if (response == 0) {
   ArrayList<String> ownedSkus = ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
   ArrayList<String> purchaseDataList = ownedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
   ArrayList<String> signatureList = ownedItems.getStringArrayList("INAPP_DATA_SIGNATURE_LIST");
   String continuationToken = ownedItems.getString("INAPP_CONTINUATION_TOKEN");

   for (int i = 0; i < purchaseDataList.size(); ++i) {
    String purchaseData = purchaseDataList.get(i);
    JSONObject object = new JSONObject(purchaseData);
    String productId = object.getString("productId");
    String purchaseToken = object.getString("purchaseToken");

    alert(productId + "," + purchaseToken);
   }
  }
 }catch(Exception e) {
  e.printStackTrace();
 }
}

10. APKを作成する

必ず署名をしてAPKを作成して下さい。
f:id:kagasu:20161218121112j:plain

署名をしないと以下のようになります。
f:id:kagasu:20161218121326j:plain

このバージョンのアプリには、Google Playを通じたお支払はご利用になれません。
詳しくはヘルプセンターをご覧ください。

11. アプリを Google Play Devloper Console にアップロードする

12. アプリ公開に必要な情報を全て入力する(コンテンツレーティングなど)

13. アプリ内課金アイテムを追加する

14. テスト用のアクセス権がある Gmail アカウントを追加する

Google Play Devloper Console の左端にある設定(歯車アイコン)→アカウントの詳細

15. アプリを公開する

公開完了まで数時間かかるので待つ。

16. 課金テストする。

購入テスト時には、必ず「これはテスト用です」と書かれていることを確認して下さい。
確認しないと実際に課金されます。
Google Play の手数料は30%です。
取引手数料

Ⅳ. 参考文献

アプリ内課金を実装する | Android Developers
https://developer.android.com/google/play/billing/billing_integrate.html?hl=ja

Androidでのアプリ内課金でハマったポイント・注意するポイント - Qiita
http://qiita.com/intel_yu/items/0d3266d028c788f3ac2d