Edited at

Androidのアプリ内購入を実装 Google Play Billing Library 2.0


注意

ライブラリのバージョンが1.0から2.0に上がったので、内容を書き換えました。以下はテスト試行のコードです。

何とか動くコードの例として示しますが、自己責任でお願いします。お金の絡む話ですので、本家の情報を確認することを強く薦めます。

https://developer.android.com/google/play/billing/billing_library_overview?hl=ja


はじめに

Androidにおけるアプリ内購入処理のサンプルを検索すると、多くが古い複雑な方法であった。

新たなGoogle Play Billing Libraryを使うと、比較的簡単に実現できたので覚えとして記す。

この手順では「IInAppBillingService.aidl」や「AndroidManifest.xml」などの設定は不要である。

また、GooglePlayにアプリをアップロードする前にテストできる。


手順


テスト用に新規プロジェクトを作成する。

ここではActivityとして、単純な「Emply Activity」を選択し、MainActivityの中に全てのコードを記述した。プロジェクト名は「BillingSample」とした。


build.gradle(Module:)のdependenciesに、billingを追加する。


build.gradle

    dependencies {

....
implementation 'com.android.billingclient:billing:2.0.1'
}


MainActivityを以下のように変更する。


MainActivity.java

  package xx.xx.xx.xx.billingsample;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchaseHistoryRecord;
import com.android.billingclient.api.PurchaseHistoryResponseListener;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity
implements View.OnClickListener, PurchasesUpdatedListener, AcknowledgePurchaseResponseListener {

TextView textView1;
private BillingClient billingClient;
List<SkuDetails> mySkuDetailsList;

// アプリ開始時に呼ばれる
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 操作ボタンと結果出力欄を準備する
textView1 = findViewById(R.id.text_view1);
findViewById(R.id.button_get_skus).setOnClickListener(this);
findViewById(R.id.button_query_owned).setOnClickListener(this);
findViewById(R.id.button_purchase).setOnClickListener(this);
findViewById(R.id.button_purchase_history).setOnClickListener(this);

// BillingClientを準備する
billingClient = BillingClient.newBuilder(this)
.setListener(this).enablePendingPurchases().build();
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
int responseCode = billingResult.getResponseCode();
if (responseCode == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
textView1.setText("Billing Setup OK");
} else {
showResponseCode(responseCode);
}
}

@Override
public void onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
textView1.setText("Billing Servise Disconnected. Retry");
}
});
}

// ボタンクリック時に呼ばれる
@Override
public void onClick(View v) {
if (v != null) {
switch (v.getId()) {
case R.id.button_get_skus:
querySkuList();
break;

case R.id.button_query_owned:
queryOwned();
break;

case R.id.button_purchase:
startPurchase("android.test.purchased");
break;

case R.id.button_purchase_history:
queryPurchaseHistory();
break;
default:
break;
}
}
}

// アプリ終了時に呼ばれる
@Override
protected void onDestroy() {
billingClient.endConnection();
super.onDestroy();
}

// 購入したいアイテムを問い合わせる
void querySkuList() {
List skuList = new ArrayList<>();
skuList.add("android.test.purchased"); // prepared by Google
skuList.add("android.test.canceled");
skuList.add("android.test.refunded");
skuList.add("android.test.item_unavailable");
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
billingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult,
List<SkuDetails> skuDetailsList) {
// Process the result.
StringBuffer resultStr = new StringBuffer("");
int responseCode = billingResult.getResponseCode();
if (responseCode == BillingClient.BillingResponseCode.OK) {
// 後の購入手続きのためにSkuの詳細を保持
mySkuDetailsList = skuDetailsList;
// リストを表示
if (skuDetailsList != null) {
for (Object item : skuDetailsList) {
SkuDetails skuDetails = (SkuDetails) item;
String sku = skuDetails.getSku();
String price = skuDetails.getPrice();
resultStr.append("Sku=" + sku + " Price=" + price + "\n");
}
} else {
resultStr.append("No Sku");
}
textView1.setText(resultStr);
} else {
showResponseCode(responseCode);
}
}
});
}

// 購入処理を開始する
void startPurchase(String sku) {
SkuDetails skuDetails = getSkuDetails(sku);
if (skuDetails != null) {
BillingFlowParams params = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
BillingResult billingResult = billingClient.launchBillingFlow(this, params);
showResponseCode(billingResult.getResponseCode());
}
}

// 指定したSKUの詳細をリスト内から得る
SkuDetails getSkuDetails(String sku) {
SkuDetails skuDetails = null;
if(mySkuDetailsList==null){
textView1.setText("Exec [Get Skus] first");
}else {
for (SkuDetails sd : mySkuDetailsList) {
if (sd.getSku().equals(sku)) skuDetails = sd;
}
if (skuDetails == null) {
textView1.setText(sku + " is not found");
}
}
return skuDetails;
}

// 購入結果の更新時に呼ばれる
@Override
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
StringBuffer resultStr = new StringBuffer("");
int billingResultCode = billingResult.getResponseCode();
if (billingResultCode == BillingClient.BillingResponseCode.OK
&& purchases != null) {
for (Purchase purchase : purchases) {
//購入を承認する
String state = handlePurchase(purchase);
//購入したSkuの文字列と承認結果を表示する
String sku = purchase.getSku();
resultStr.append(sku).append("\n");
resultStr.append(" State=").append(state).append("\n");
}
textView1.setText(resultStr);
} else {
// Handle error codes.
showResponseCode(billingResultCode);
}
}

// 購入を承認する
String handlePurchase(Purchase purchase) {
String stateStr = "error";
int purchaseState = purchase.getPurchaseState();
if (purchaseState == Purchase.PurchaseState.PURCHASED) {
// Grant entitlement to the user.
stateStr = "purchased";
// Acknowledge the purchase if it hasn't already been acknowledged.
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.acknowledgePurchase(acknowledgePurchaseParams, this);
}
}else if(purchaseState == Purchase.PurchaseState.PENDING){
stateStr = "pending";
}else if(purchaseState == Purchase.PurchaseState.UNSPECIFIED_STATE){
stateStr = "unspecified state";
}
return stateStr;
}

// 購入承認の結果が戻る
@Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
int responseCode = billingResult.getResponseCode();
if(responseCode != BillingClient.BillingResponseCode.OK) {
showResponseCode(responseCode);
}
}

// 購入済みアイテムを問い合わせる(キャッシュ処理)
void queryOwned(){
StringBuffer resultStr = new StringBuffer("");
Purchase.PurchasesResult purchasesResult
= billingClient.queryPurchases(BillingClient.SkuType.INAPP);
int responseCode = purchasesResult.getResponseCode ();
if(responseCode== BillingClient.BillingResponseCode.OK){
resultStr.append("Query Success\n");
List<Purchase> purchases = purchasesResult.getPurchasesList();
if(purchases.isEmpty()){
resultStr.append("Owned Nothing");
} else {
for (Purchase purchase : purchases) {
resultStr.append(purchase.getSku()).append("\n");
}
}
textView1.setText(resultStr);
}else{
showResponseCode(responseCode);
}
}

// 購入履歴を問い合わせる(ネットワークアクセス処理)
void queryPurchaseHistory() {
billingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP,
new PurchaseHistoryResponseListener() {
@Override
public void onPurchaseHistoryResponse(BillingResult billingResult,
List<PurchaseHistoryRecord> purchasesList) {
int responseCode = billingResult.getResponseCode();
if (responseCode == BillingClient.BillingResponseCode.OK) {
if (purchasesList == null || purchasesList.size() == 0) {
textView1.setText("No History");
} else {
for (PurchaseHistoryRecord purchase : purchasesList) {
// Process the result.
textView1.setText("Purchase History="
+ purchase.toString() + "\n");
}
}
} else {
showResponseCode(responseCode);
}
}
});
}
// サーバの応答を表示する
void showResponseCode(int responseCode){
switch(responseCode){
case BillingClient.BillingResponseCode.OK:
textView1.setText("OK");break;
case BillingClient.BillingResponseCode.USER_CANCELED:
textView1.setText("USER_CANCELED");break;
case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE:
textView1.setText("SERVICE_UNAVAILABLE");break;
case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE:
textView1.setText("BILLING_UNAVAILABLE");break;
case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE:
textView1.setText("ITEM_UNAVAILABLE");break;
case BillingClient.BillingResponseCode.DEVELOPER_ERROR:
textView1.setText("DEVELOPER_ERROR");break;
case BillingClient.BillingResponseCode.ERROR:
textView1.setText("ERROR");break;
case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
textView1.setText("ITEM_ALREADY_OWNED");break;
case BillingClient.BillingResponseCode.ITEM_NOT_OWNED:
textView1.setText("ITEM_NOT_OWNED");break;
case BillingClient.BillingResponseCode.SERVICE_DISCONNECTED:
textView1.setText("SERVICE_DISCONNECTED");break;
case BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED:
textView1.setText("FEATURE_NOT_SUPPORTED");break;
}
}
}



レイアウトファイル「activity_main.xml」を以下のように変更する。


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<Button
android:id="@+id/button_get_skus"
android:text="Get Skus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/button_query_owned"
android:text="Query Owned"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/button_purchase"
android:text="Purchase"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/button_purchase_history"
android:text="Purchase History"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/text_view1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</android.support.constraint.ConstraintLayout>



Build ValiantをReleaseに変更する。

AndroidStudioの左端タブからBuild Valiantsを開いて、Build ValiantをDebugからReleaseに変更する。


実機にインストールしようとすると、以下のエラーが表示される。


Error: The apk for your currently selected variant (app-release-unsigned.apk) is not signed.



Keyを入力する。

エラー表示の右横に表示された「Fix」をクリックしてGooglePlayに登録しているキーを入力する。未登録なら登録する必要がある。キーの入力手順は以下の通り。

1. 「Fix」を押すと、「Project Structure>Modules>Signing Configs」タブが表示される。

2. 「+」を押して、Keyを追加する。nameは「config」のままで良い。

3. Google Playに登録しているキーの情報(storeFile, storePassword, keyAlias, keyPassword)を入力する。

3. 「Default Config」タブを選択して、「Signing Config」にて、右端選択▼から、追加したキーの名前(config)を選択する。


実機でテストする。


  1. アプリをスタートすると、「Setup Success」を表示する。

  2. 「Get Skus」ボタンをクリックすると、問い合わせた4つのアイテムの情報を表示する。

  3. 「Query Owned」 ボタンをクリックすると、購入済みのアイテムを表示する。最初は無しである。

  4. 「Purchase」ボタンをクリックすると、「android.test.purchased」を購入するためのダイアログを表示する。OKをクリックで購入する。

  5. 再度「Query Owned」 ボタンをクリックすると、今購入したアイテム「android.test.purchased」を表示する。


注意


  • テストは、実機上でRelease版を使って行う必要がある。仮想端末上では失敗する。

  • ここで操作している4つのアイテムは、Googleがテスト用に用意したものである。これらはアプリをGooglePlayにアップロードしなくても使うことができる。

  • 「android.test.purchased」は購入が成功するアイテムである。実際の支払いは発生しない。購入済みの状態は、約1日が経過するとリセットされる。

  • 自分独自の販売アイテムはアプリをGooglePlayにアップロードしてから、GooglePlay内で設定する。

  • 当然ながら、これは各機能をテストするための構成である。実際には、クラス分けやエラー対応、問合せのタイミングなどに工夫が必要である。

  • 「QUERY OWNED」はデバイス内のキャッシュをチェックする。再インストールや他端末による購入は正しく反映されない。「PURCHASE HISTORY」は、ネットワークアクセスでそれらの場合もチェックするはずが、確認できていない。