Android用アプリ内課金(IAP)APIを組み込む方法


Android用アプリ内課金(IAP)APIを組み込む方法

このページでは、Androidアプリにアプリ内課金(IAP)APIを組み込む方法について説明し、IAP v2.0 APIリファレンスドキュメント(英語のみ)の補足となるユースケースとコードスニペットを紹介します。このドキュメント内のコードスニペットの多くは、消費型アイテムのIAPサンプルアプリに含まれています。

Android IAPパッケージについて

com.amazon.device.iapパッケージには、Androidアプリにアプリ内課金を実装するために使用するクラスとインターフェイスが用意されています。

このパッケージには、次のインターフェイスとクラスが含まれています。

  • ResponseReceiver: Amazonアプリストアからのブロードキャストインテントを受信するクラス
  • PurchasingService: Amazonアプリストアを通じてリクエストを開始するクラス
  • PurchasingListenerPurchasingServiceによって開始されたリクエストに対する非同期レスポンスを受け取るインターフェイス

以下の表に、PurchasingServiceのリクエストメソッドと、各メソッドに対応するPurchasingListenerのレスポンスコールバックを示します。IAP APIを実装する場合は、これらのメソッド、コールバック、レスポンスオブジェクトを頻繁に使用することになります。

PurchasingServiceのメソッド PurchasingListenerのコールバック レスポンスオブジェクト
getUserData() onUserDataResponse() UserDataResponse
getPurchaseUpdates() onPurchaseUpdatesResponse() PurchaseUpdatesResponse
getProductData() onProductDataResponse() ProductDataResponse
purchase() onPurchaseResponse() PurchaseResponse
notifyFulfillment() なし なし

これらのクラスとメソッドの詳細については、IAP v 2.0 APIリファレンスドキュメント(英語のみ)を参照してください。

ResponseReceiver

アプリ内課金(IAP)APIは、すべての処理を非同期に実行します。アプリでは、ResponseReceiverクラスを通じてAmazonアプリストアからブロードキャストインテントを受信する必要があります。このクラスがアプリ内で直接使用されることはありませんが、アプリでインテントを受信するには、AndroidManifest.xmlファイルにResponseReceiverのエントリを追加する必要があります。次のコードスニペットは、IAP v2.0用のAndroidManifest.xmlファイルにResponseReceiverを追加する方法を示しています。

 <application>
  ...
  <receiver android:name = "com.amazon.device.iap.ResponseReceiver"
      android:permission = "com.amazon.inapp.purchasing.Permission.NOTIFY" >
    <intent-filter>
      <action android:name = "com.amazon.inapp.purchasing.NOTIFY" />
    </intent-filter>
  </receiver>
  ...
 </application>

PurchasingService

PurchasingServiceクラスは、さまざまなタイプの情報を取得し、購入を実行して、購入されたアイテムの付与完了をAmazonに通知します。PurchasingServiceには、次のメソッドが実装されています。

  • registerListener(PurchasingListener purchasingListener)PurchasingServiceクラスのほかのメソッドを呼び出す前に、このメソッドを呼び出す必要があります。
  • getUserData(): 現在ログオンしているユーザーのアプリ固有IDとマーケットプレイスを取得するには、このメソッドを呼び出します。たとえば、ユーザーがアカウントを切り替えた場合や、同じデバイスで複数のユーザーがアプリにアクセスした場合、この呼び出しにより、取得するレシートが現在のユーザーアカウントのものであることを確認できます。
  • getPurchaseUpdates(boolean reset): すべてのデバイスを対象として、定期購入型アイテムと非消費型アイテムの購入情報を取得します。消費型アイテムの購入情報は、購入を行ったデバイスからのみ取得できます。getPurchaseUpdatesでは、付与が完了していない消費型アイテムとキャンセルされた消費型アイテムの購入情報だけが取得されます。このメソッドから返されたPurchaseUpdatesResponseデータを保持しておき、その後の呼び出しでは更新分だけをシステムに問い合わせることをお勧めします。レスポンスはページ分割されます。
  • getProductData(java.util.Set skus): アプリに表示するSKUセットのアイテムデータを取得するには、このメソッドを呼び出します。
  • purchase(java.lang.String sku): 特定のSKUの購入を開始するには、このメソッドを呼び出します。
  • notifyFulfillment(java.lang.String receiptId, FulfillmentResult fulfillmentResult): 指定したreceiptIdFulfillmentResultを送信するには、このメソッドを呼び出します。FulfillmentResultに指定できる値は、FULFILLEDまたはUNAVAILABLEです。

PurchasingListener

非同期コールバックを処理するには、PurchasingListenerインターフェイスを実装します。これらのコールバックはUIスレッドで呼び出されるため、長時間実行されるタスクをUIスレッドで処理しないようにしてください。PurchasingListenerのインスタンスには、次のメソッドを実装する必要があります。

  • onUserDataResponse(UserDataResponse userDataResponse): getUserData()の呼び出し後に呼び出されます。現在ログオンしているユーザーのUserIdmarketplaceを特定します。
  • onPurchaseUpdatesResponse(PurchaseUpdatesResponse purchaseUpdatesResponse)getPurchaseUpdates(boolean reset)の呼び出し後に呼び出されます。購入履歴を取得します。このメソッドから返されたPurchaseUpdatesResponseデータを保持しておき、その後の呼び出しでは更新分だけをシステムに問い合わせることをお勧めします。
  • onProductDataResponse(ProductDataResponse productDataResponse)getProductDataRequest(java.util.Set skus)の呼び出し後に呼び出されます。アプリで販売するSKUに関する情報を取得します。onPurchaseResponse()では、この呼び出しで取得された有効なSKUを使用します。
  • onPurchaseResponse(PurchaseResponse purchaseResponse)purchase(String sku)の呼び出し後に呼び出されます。購入のステータスを判断するために使用します。

レスポンスオブジェクト

PurchasingServiceを通じて呼び出しを開始すると、その結果として、対応するレスポンスがPurchasingListenerに送られます。これらのレスポンスでは、それぞれ1つのレスポンスオブジェクトが使用されます。

  • UserDataResponse: 現在ログオンしているユーザーのアプリ固有のUserIdmarketplaceを提供します。
  • PurchaseUpdatesResponse: ページ分割されたレシートのリストを提供します。レシートはソートされていません。
  • ProductDataResponse: SKUをキーとするアイテムデータを提供します。利用不可能なSKUのリストはgetUnavailableSkus()メソッドで取得できます。
  • PurchaseResponse: アプリ内で開始された購入のステータスを提供します。PurchaseResponse.RequestStatusの示す結果がFAILEDになる理由には、単にユーザーが購入手続きをキャンセルしただけの場合もあることに注意してください。

IAP APIをアプリに組み込む方法

ここまで、IAPの実装に必要なクラスについて少し詳しく説明してきました。次は、アプリにIAPのコードを記述していきます。

このセクションのコードスニペットは、SDKに付属する消費型アイテムのIAPサンプルアプリから引用したものです。

1.プレースホルダーメソッドの作成

コードの骨組みを作るために、以下の場所で以下のメソッドを呼び出すプレースホルダーまたはスタブコードを作成します。

  • onCreate()メソッド内でregisterListener()を呼び出す。
  • onResume()メソッド内でgetUserData()を呼び出す。
  • onResume()メソッド内でgetPurchaseUpdates()を呼び出す。
  • onResume()メソッド内でgetProductData()を呼び出す。

これら4つの呼び出しはPurchasingServiceクラスの一部であり、アプリ内課金を実行するための基盤となります。以降の手順では、上記の呼び出しの実装方法について詳しく説明し、独自のコードを記述するときにモデルとして使用できるサンプルコードを紹介します。

2.PurchasingListenerの実装と登録

ResponseReceiverによってトリガーされるコールバックをアプリでリッスンして処理できるように、コードにPurchasingListenerを実装して登録します。前の手順で設定した呼び出しは次のようになります。

private SampleIapManager sampleIapManager;
protected void onCreate(final Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setupApplicationSpecificOnCreate();
  setupIAPOnCreate();
}

private void setupIAPOnCreate() {
  sampleIapManager = new SampleIapManager(this);

  final SamplePurchasingListener purchasingListener = new SamplePurchasingListener(sampleIapManager);
  Log.d(TAG, "onCreate: PurchasingListener を登録します");

  PurchasingService.registerListener(this.getApplicationContext(), purchasingListener);

  Log.d(TAG, "IS_SANDBOX_MODE:" + PurchasingService.IS_SANDBOX_MODE);
}

このサンプルコードでは、PurchasingListenerを登録するだけでなく、次の2つのオプションのタスクを実行しています。

  • 購入レシートに関連するデータを格納するための新しいSampleIapManagerインスタンスを作成する。このタスクは省略できますが、その場合はアクセス可能なほかの場所に購入レシートデータを格納する必要があります。データベースを使用するかメモリ内にデータを格納するかは開発者が決定します。
  • PurchasingService.IS_SANDBOX_MODEをチェックして、アプリがサンドボックスモードで動作しているかどうかを調べる。このフラグは、アプリの開発中、App Testerを使用してローカルでアプリをテストするときに役立つ可能性があります。

3.ユーザー情報の取得

onResume()getUserData()を呼び出して、現在のユーザーに関する情報(ユーザーIDとマーケットプレイス)を取得します。

// ...
private String currentUserId =  null ;
private String currentMarketplace =  null ;

// ...

public void onUserDataResponse( final UserDataResponse response) {

 final UserDataResponse.RequestStatus status = response.getRequestStatus();

 switch (status) {
   case SUCCESSFUL:
	 currentUserId = response.getUserData().getUserId();
	 currentMarketplace = response.getUserData().getMarketplace();
	 break ;

   case FAILED:
   case NOT_SUPPORTED:
	 // 失敗時の処理を適切に行います。
	 break ;
 }
}

この例では、後で使用できるようにユーザーIDとマーケットプレイスをメモリ内に保持しています。

4. getPurchaseUpdatesメソッドの実装

onResume()getPurchaseUpdates()の呼び出しを実装します。このメソッドでは、前回の呼び出し以降にユーザーが行った購入トランザクションをすべて取得できます。

  • resetfalseに設定すると、レスポンスとして、前回getPurchaseUpdates()が呼び出された後の購入履歴がページ分割されて返されます。この呼び出しで、ユーザーの保留中の消費型アイテム、非消費型アイテム、定期購入型アイテムの購入に関するレシートが取得されます(Amazonでは、基本的にこのアプローチを使用することを推奨します)。
  • ユーザーの購入履歴全体を取得して、サーバー側のデータキャッシュなどに保存したり、メモリ内にすべてを保持したりする場合に限り、resettrueに設定します。

resetフラグの値にかかわらず、getPurchaseUpdates()は次のように動作する点に注意してください。

  • getPurchaseUpdates()呼び出しは、アイテム付与が完了していないすべての消費型アイテムの購入情報を常に返します。
  • ごくまれに、アイテム付与が完了している購入情報がgetPurchaseUpdates()の呼び出しから返されることがあります。たとえば、アイテムを付与した後、Amazonに通知する前にアプリがクラッシュした場合や、アイテムの付与後にAmazon側で問題が発生した場合は、付与が完了している消費型アイテムの購入情報がgetPurchaseUpdates()から返されます。このような状況では、アイテムを二重に付与することを避けるために、重複しているレシートを無視する必要があります。アイテムの配信時は、何らかの方法で配信状況を記録しておき、2つ目のレシートを受け取っても二重に配信しないようにしてください。

getPurchaseUpdates()からレスポンスが返されると、PurchasingListener.onPurchaseUpdatesResponse()コールバックがトリガーされます。

@Override
protected void onResume() {
  super.onResume();

//...

  PurchasingService.getUserData();

//...

  PurchasingService.getPurchaseUpdates(false);
}

続いて、レスポンスの処理を行います。

PurchasingListener.onPurchaseUpdatesResponse()コールバックがトリガーされたら、PurchaseUpdatesResponse.getPurchaseUpdatesRequestStatus()から返されるリクエストステータスを確認します。requestStatusがSUCCESSFULの場合は、各レシートを処理します。

ページ分割を処理するには、PurchaseUpdatesResponse.hasMore()の値を取得します。PurchaseUpdatesResponse.hasMore()からtrueが返された場合は、次のサンプルコードに示すように、getPurchaseUpdates()の再帰呼び出しを行います。

public class MyPurchasingListener  implements PurchasingListener {
   boolean reset =  false ;
   //...

   public void onPurchaseUpdatesResponse( final PurchaseUpdatesResponse response) {
     //...
     // レシートを処理します
     switch (response.getPurchaseUpdatesRequestStatus()) {
       case SUCCESSFUL:
         for ( final Receipt receipt : response.getReceipts()) {
           // レシートを処理します
         }
         if (response.hasMore()) {
           PurchasingService.getPurchaseUpdates(reset);
         }
         break ;
       case FAILED:
         break ;
     }
   }
   //...
}

5.getProductDataメソッドの実装

同じくonResume()メソッド内で、getProductData()を呼び出してSKUを検証します。これは、無効なSKUが原因でユーザーの購入が失敗することを避けるためです。

次のサンプルコードでは、アプリの消費型アイテム、非消費型アイテム、定期購入型アイテムのSKUをAmazonで検証します。

protected void onResume() {
   super.onResume();

   // ...

   final Set <string>productSkus =  new HashSet<string>();
   productSkus.add( "com.amazon.example.iap.consumable" );
   productSkus.add( "com.amazon.example.iap.entitlement" );
   productSkus.add( "com.amazon.example.iap.subscription" );
   PurchasingService.getProductData(productSkus);

   Log.v(TAG,  "SKUをAmazonで検証します" );
   }

PurchasingService.getProductData()メソッドを呼び出すと、PurchasingListener.onProductDataResponse()コールバックが呼び出されます。ProductDataResponse.getRequestStatus()から返されたリクエストステータスを確認し、この呼び出しによって検証されたアイテムまたはSKUのみを販売します。

成功したリクエスト

requestStatusSUCCESSFULの場合は、アプリに表示するSKUをキーとして商品データマップを取得します。商品データマップには次の値が含まれます。

  • 商品タイプ
  • アイコンのURL
  • ローカライズされた価格(定期購入型アイテムの子SKUの場合)
  • タイトル
  • 説明
  • SKU

アプリ内でIAPアイコンを表示する場合は、AndroidManifest.xmlファイルを編集して、android.permission.INTERNETパーミッションを含める必要があります。

また、requestStatusSUCCESSFULであっても、利用不可能なSKUが存在する場合は、PurchaseUpdatesResponse.getUnavailableSkus()を呼び出して無効なSKUの商品データを取得し、アプリのユーザーがそれらの商品を購入できないようにしてください。

失敗したリクエスト

requestStatusFAILEDの場合は、次のサンプルコードに示すように、アプリのIAP機能を無効にします。

public class MyPurchasingListener  implements PurchasingListener {
   //  ...

   public void onProductDataResponse( final ProductDataResponse response) {
     switch (response.getRequestStatus()) {
       case SUCCESSFUL:
         for ( final String s : response.getUnavailableSkus()) {
           Log.v(TAG,  "利用不可能な SKU:" + s);
         }

         final Map <string,>products = response.getProductData();
         for ( final String key : products.keySet()) {
           Product product = products.get(key);
           Log.v(TAG, String.format( "商品:%s\n タイプ:%s\n SKU:%s\n 価格:%s\n 説明: %s\n" , product.getTitle(), product.getProductType(), product.getSku(), product.getPrice(), product.getDescription()));
         }
         break ;

       case FAILED:
         Log.v(TAG,  "ProductDataRequestStatus: FAILED" );
         break ;
     }
   }

   //  ...
}

6.購入を実行するコードの実装

購入を実行するコードを記述します。ここに示す例では消費型アイテムの購入を実行しますが、定期購入型アイテムや非消費型アイテムでも同様のコードを使用できます。

次のコードはMainActivityからの抜粋で、PurchasingService.purchase()を呼び出して購入を開始します。消費型アイテムのサンプルアプリでは、アプリユーザーが [Buy Orange] ボタンをタップすると、このメソッドが実行されます。

  public void onBuyOrangeClick(final View view) {
     final RequestId requestId = PurchasingService.purchase(MySku.ORANGE.getSku());
     Log.d(TAG, "onBuyOrangeClick: requestId (" + requestId + ")");
  }

次に、SamplePurchasingListener.onPurchaseResponse()コールバックを実装します。以下のコードでは、実際の購入処理はSampleIapManager.handleReceipt()によって行われます。

public void onPurchaseResponse(final PurchaseResponse response) {
	switch (status) {
		// ...
		case SUCCESSFUL:
			final Receipt receipt = response.getReceipt();
			iapManager.setAmazonUserId(response.getUserData().getUserId(), response.getUserData().getMarketplace());
			Log.d(TAG, "onPurchaseResponse: receipt json:" + receipt.toJSON());
			iapManager.handleReceipt(receipt, response.getUserData());
			iapManager.refreshOranges();
			break;
	}
}

7.購入の完了と購入レシートの処理

購入を完了し、購入レシートを処理します。独自のアプリを設計するときは、多くの場合、これらの手順をすべて1か所で処理するアイテム付与のしくみを実装することになります。

購入可能アイテムが定期購入型アイテムである場合は、receiptIdの値に関して次の点に注意してください。

*   定期購入が継続されていて途中でキャンセルされたことがない場合、その定期購入型アイテム/ユーザーについてアプリが受け取るレシートは1つだけです。 

*   定期購入が継続的でない場合、たとえば、ユーザーが自動更新を選択せず、定期購入が期限切れになり、その1か月後に再び定期購入を開始した場合、アプリは複数のレシートを受け取ります。

アイテムを付与する前に、購入のレシートを検証します。これを行うには、バックエンドサーバーでAmazonのレシート検証サービス(RVS)を使用してreceiptIdを検証します。Amazonでは、RVS Sandbox環境とRVS本番環境の両方を提供しています。RVS SandboxとサーバーをセットアップしてRVSを使用できるようにする方法については、レシート検証サービス(RVS)の ドキュメントを参照してください。* 開発中は、RVS Sandbox環境を使用して、App Testerによって生成されたレシートを検証します。* 本番環境では、RVS本番環境エンドポイントを使用します。

以下の例では、SampleIapManagerhandleConsumablePurchase()メソッドで、レシートがキャンセルされているかどうかを確認します。

  • キャンセルされたレシートに対して既にアイテム付与が完了している場合は、revokeConsumablePurchase()メソッドを呼び出して購入を取り消します。
  • レシートがキャンセルされていない場合は、サーバーからRVSを使用してレシートを検証し、grantConsumablePurchase()を呼び出して購入アイテムを付与します。

    public void handleConsumablePurchase(final Receipt receipt, final UserData userData) {
        try {
            if (receipt.isCanceled()) {
                revokeConsumablePurchase(receipt, userData);
            } else {
                // レシートの検証はサーバー側で実行することを強く推奨します
                if (!verifyReceiptFromYourService(receipt.getReceiptId(), userData)) {
                    // 購入を検証できない場合は
                    // 適切なエラーメッセージをユーザーに表示します。
                    mainActivity.showMessage("購入を検証できませんでした。後でもう一度お試しください。");
                    return;
                }
                if (receiptAlreadyFulfilled(receipt.getReceiptId(), userData)) {
                    // レシートのアイテムが以前に付与されている場合は、付与済みであることを
                    // Amazonアプリストアに改めて通知します。
                    PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.FULFILLED);
                    return;
                }
    
                grantConsumablePurchase(receipt, userData);
            }
            return;
        } catch (final Throwable e) {
            mainActivity.showMessage("購入を完了できませんでした。もう一度お試しください。");
        }
    }
    

8.ユーザーへのアイテムの付与

アイテムをユーザーに付与するには、購入レコードを作成し、そのレコードを永続的な場所に保存します。

さらに、SKUを検証します。

  • SKUが利用可能な場合は、アイテムを付与し、ステータスFULFILLEDを指定してnotifyFulfillmentを呼び出します。この手順が完了すると、その購入レシートがAmazonアプリストアからアプリに送信されることはなくなります。
  • SKUが利用不可能な場合は、ステータスUNAVAILABLEを指定してnotifyFulfillmentを呼び出します。
private void grantConsumablePurchase(final Receipt receipt, final UserData userData) {
    try {
        // 以下のサンプルコードは簡易な実装です。独自の付与ロジックを
        // 実装するときは、スレッドセーフ、トランザクション性、堅牢性に
        // 注意してください。

        // アプリ/サーバーで購入情報を作成し、購入アイテムを
        // ユーザーに付与します。この例では、ユーザーにオレンジを1つ
        // 付与します。
        createPurchase(receipt.getReceiptId(), userData.getUserId());
        final MySku mySku = MySku.fromSku(receipt.getSku(), userIapData.getAmazonMarketplace());
        // SKUがまだ利用可能であることを確認します。
        if (mySku == null) {
            Log.w(TAG, "レシート内のSKU [" + receipt.getSku() + "] は無効になっています。");
            // SKUが利用不可能になっている場合は、ステータス「UNAVAILABLE」を指定して
            // PurchasingService.notifyFulfillmentを呼び出します。
            updatePurchaseStatus(receipt.getReceiptId(), null, PurchaseStatus.UNAVAILABLE);
            PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.UNAVAILABLE);
            return;
        }

        if (updatePurchaseStatus(receipt.getReceiptId(), PurchaseStatus.PAID, PurchaseStatus.FULFILLED)) {
            // SQLiteデータベースの購入ステータスの更新に成功
            userIapData.setRemainingOranges(userIapData.getRemainingOranges() + 1);
            saveUserIapData();
            Log.i(TAG, "購入ステータスをPAIDからFULFILLEDに正しく更新しました。レシートID:" + receipt.getReceiptId());
            // Amazonアプリストアにステータスの更新を渡します。購入に対して付与完了の
            // ステータスが通知されると、Amazonはその購入レシートをアプリに送信しなく
            // なくなります。
            PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.FULFILLED);
        } else {
            // SQLiteデータベースの購入ステータスの更新に失敗 - ステータスは
            // 既に変更されています。
            // これは通常、同じレシートが別のonPurchaseResponseまたは
            // onPurchaseUpdatesResponseコールバックで更新されたことを意味します。
            // このサンプルコードでは、エラーを表示せずにログのみを出力します。
            Log.w(TAG, "購入ステータスをPAIDからFULFILLEDに更新できませんでした。レシートID:" + receipt.getReceiptId()
                       + "。ステータスは既に変更されています。");
        }

    } catch (final Throwable e) {
        // 何らかの理由でアプリが購入アイテムを付与できない場合のために、
        // ここに独自のエラー処理コードを追加します。
        // 次にPurchasingService.getPurchaseUpdates APIを呼び出すと、
        // Amazonから消費型アイテムの購入レシートが再度送信されます。
        Log.e(TAG, "購入された消費型アイテムを付与できませんでした。エラー:" + e.getMessage());
    }
}

9.レシートの処理

レシートを処理します。この手順はgetPurchaseUpdates()の結果として実行し、purchase(SKU)呼び出しの結果ではないことに注意してください。SampleIapManager.handleReceiptメソッドを呼び出して、PurchaseUpdatesResponseの一部として返されたすべてのレシートを処理します。

public void onPurchaseUpdatesResponse(final PurchaseUpdatesResponse response) {
// ....
switch (status) {
case SUCCESSFUL:
	iapManager.setAmazonUserId(response.getUserData().getUserId(), response.getUserData().getMarketplace());
	for (final Receipt receipt : response.getReceipts()) {
		iapManager.handleReceipt(receipt, response.getUserData());
	}
	if (response.hasMore()) {
		PurchasingService.getPurchaseUpdates(false);
	}
	iapManager.refreshOranges();
	break;
// ...