Androidアプリ定期購入(月額課金など)について

このエントリーは、エキサイト Advent Calendar 2016の12/21の記事です。最終日のクリスマスまであと4日!本日は昨日に引き続き、Androidに関することを書きたいと思います。

E・レシピ担当の長谷川です。以前にも「Apache Solrによる検索サジェスト機能の実現」というエンジニアブログを執筆しました。

WEBエンジニア大募集!

エキサイトではWEBエンジニアを中途採用で募集しています。
☆キャリアチェンジも大歓迎☆
充実した研修制度で成長したい方、ぜひこちらからご応募ください。


11月末にE・レシピのプレミアムメンバー機能を実装したAndroidアプリをリリースしました。こちらは既にリリースされているアプリに課金機能を追加した形になります。そこで本日はAndroidアプリの定期購入(the In-app Billing Version 3 API)について書きたいと思います。内容が不正確である可能性もありますので、公式ページをご参考にして実装を行ってください。

Androidアプリ定期購入(月額課金など)について_f0364156_11532216.png

課金処理のフロー

まずアプリで購入情報を行い、サーバーでレシートの検証を行います。

Androidアプリ定期購入(月額課金など)について_f0364156_12411008.png
  1. Google Playにユーザーのアプリ内購入情報を照会し、購入リクエスト送信
  2. developerPayloadのチェック
  3. 購入情報をサーバーへ送信
  4. 署名検証
  5. Google Play Developer APIを使って購入の詳細情報を取得
  6. 課金処理(会員登録など)
  7. アプリ内課金処理(有効期限登録など)

商品リスト作成

商品リストではアプリ内で販売するアイテム(定期購入)を指定します。「Google Play Developer Console」 -> 実装アプリ -> アプリ内アイテム -> 新しいアイテムを追加を選択します。アイテムID(この値はアプリでも使用します)やタイプ、価格などを設定します。1度設定すると変更できない箇所も存在する重要な設定項目ということもあると思いますので、慎重に設定しましょう(この辺りは何度も確認しました)。

アプリ内課金実装

アプリ内課金を実装する | Android Developers」に実装方法が記載されています。また、「Android SDK」で提供されているサンプルアプリもあるので、参考にしながら実装を進めると良いと思います。

AIDLファイルをプロジェクトに追加

まず、「SDK Manager」で「Google Play Billing Library」をダウンロードをします。そして下記から「In-app Billing Version 3」サービスへのインターフェイスを定義する AIDLファイルを取得します。

{ANDROID_SDK_PATH}/extras/google/play_billing/IInAppBillingService.aidl

取得した「IInAppBillingService.aidl」を下記にコピーします。

{PROJECT_PATH}/app/src/main/com/android/vending/billing/IInAppBillingService.aidl

最後にアプリをビルドします。プロジェクト内の「/gen」ディレクトリに「IInAppBillingService.java」が生成されれば完了です。

ヘルパークラス追加

「Android SDK」で提供されているサンプルアプリの下記パスにあるヘルパークラスを課金実装を行うプロジェクトにコピーします。このヘルパークラスを利用してアプリ内課金を実装していきます。このような手順にはあまり馴染みがなかったため驚きましたが、公式ページにもきちんと記載がされていました。

{SAMPLE_PROJECT_PATH}/app/src/main/java/com/example/android/trivialdrivesample/util

AndroidManifest更新

アプリが適切なパーミッションをリクエストする必要があるので、アプリ内課金パーミッションを宣言します。

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

IabHelperセットアップ

「Google Play」とのコネクションを繋ぐために、「IabHelper」をセットアップします。「base64EncodedPublicKey」は「Google Play Developer Console」 -> 実装アプリ -> サービスとAPI -> このアプリのライセンスキーから取得することができます。

IabHelper mHelper;
@Override
public void onCreate(Bundle savedInstanceState) {
/* アプリのライセンスキー */
String base64EncodedPublicKey;

mHelper = new IabHelper(this, base64EncodedPublicKey);
mHelper.enableDebugLogging(BuildConfig.DEBUG);

/* IabHelper初期化 */
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
public void onIabSetupFinished(IabResult result) {
if (!result.isSuccess()) {
/* セットアップ失敗処理 */
return;
}

if (mHelper == null) {
return;
}

/* セットアップ完了 */
/* セットアップ完了後処理 */
}
});
}

Activity終了時には必ずアプリ内課金サービスのバインドを解除するようにします。

@Override
public void onDestroy() {
super.onDestroy();
if (mHelper != null) {
mHelper.disposeWhenFinished();
}
mHelper = null;
}

ベータ(アルファ)版テスト公開

課金テストを行うために、「Google Play Developer Console」で設定を行います。設定 -> ライセンステストにテストアカウントを登録します。そして、課金の準備を行ったAPK(アプリのバージョンは変更しておく)をアプリの(クローズド)ベータ(アルファ)版テストなどにアップロードして、テストアカウントを設定します。これが反映されると課金テストが可能になります。誤って製品版に後悔しないように気をつけましょう。この1度ベータ版にあげなければいけない手順は最初ヒヤヒヤさせられたので、テスト段階では別のテスト方法があれば安心なのですが。。。(テストアイテムを使ったテスト方法もあります!)

購入情報取得

購入情報を取得します。リスナーを設定し、処理が完了次第そのリスナーが呼ばれます。「SKU_PREMIUM」の部分には事前に作成したアイテムIDを指定します。購入している場合には「purchase」に購入情報が入ってきます。この購入情報を元にUIのアップデートを行うなどの処理を行います。

try {
mHelper.queryInventoryAsync(mGotInventoryListener);
} catch (IabAsyncInProgressException e) {
/* 購入情報取得失敗処理 */
}
/* 購入情報取得完了処理 */
IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
if (mHelper == null) {
return;
}

if (result.isFailure()) {
/* 購入情報取得失敗処理 */
return;
}

Purchase purchase = inventory.getPurchase(SKU_PREMIUM);
if (purchase == null) {
/* 未購入状態処理 */
return;
}

/* 購入状態処理 */
}
};

購入

購入処理も同様にヘルパークラスを利用して課金処理を行い、セットしたリスナー内で、結果を取得し、購入処理を行います。「REQUEST_PURCHASE」にはリクエストコードを、「payload」には「Google Play」から返る結果に埋め込まれる値を設定します。この値は開発者が自由に設定できますが、しばしばユーザ−IDを設定することが多く、購入者の判別に利用できます。

try {
mHelper.launchSubscriptionPurchaseFlow(
this,
SKU_PREMIUM,
REQUEST_PURCHASE,
mPurchaseFinishedListener,
payload
);
} catch (IabAsyncInProgressException e) {
/* 購入失敗処理 */
}
/* 課金完了処理 */
IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
if (mHelper == null) {
return;
}

if (result.isFailure()) {
/* 課金失敗処理 */
return;
}

/*
* developerPayloadのチェック
* 購入時に設定した「payload」と比較することで、購入者チェックをすることができます
*/
if (purchase.getDeveloperPayload() != payload) {
/* 課金無効処理 */
return;
}

/* アイテムID毎に処理を分けることができます */
if (purchase.getSku().equals(SKU_PREMIUM)) {
/* 購入処理 */
}
}
};

購入結果を受け取るために、「onActivityResult」をオーバーライドして、下記のように記述する必要があります。このコードがないと結果を受け取ることができません。初めこの処理を忘れていて、コールバックが呼ばれない現象が起こり混乱しました。

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (mHelper == null) {
return;
}

if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {
super.onActivityResult(requestCode, resultCode, data);
}
}

署名検証

「purchase」から下記のような購入情報を取得することができます。

mItemTypeアプリ内アイテムの場合 : inapp 定期購入の場合 : subs
mOrderId注文識別子(テスト購入場合は空欄)
mPackageNameアプリケーションパッケージ
mSkuアイテムの商品識別子
mPurchaseTime商品が購入された時間(1970年1月1日から始まるミリ秒)
mPurchaseState購入状況(0 : 購入済み 1 : キャンセル済み 2 : 返金済み)
mDeveloperPayload開発者が指定する文字列
mToken任意のアイテムとユーザーのペアにおける購入を一意に識別するトークン
mOriginalJson注文の詳細情報を含むJSON形式の文字列
mSignature秘密鍵で署名された購入データの署名を含む文字列
mIsAutoRenewing定期購入が自動更新されたかどうか

「レシート(mOriginalJson)」「署名(mSignature)」「ユーザーID」をサーバーに送り、署名検証を行います。Base64エンコードのRSA公開鍵であるアプリのライセンスキーからPEM形式の証明書を生成し、これを「openssl_get_publickey」に渡せば、PHPで公開鍵を生成することができます。そして「レシート」のSHA1ハッシュ値と「公開鍵」で復号した「署名」を使用して、検証を行います。

/**
* 署名検証
*
* @param string $receipt レシート
* @param string $signature 署名
*
* @return boolean 署名検証結果
*/
private function verifySignature($receipt, $signature)
{
/* GooglePlayのアプリの公開鍵をPEM形式に変換したもの */
$publicKey = openssl_get_publickey($this->pemPublicKey());
/* POSTでデータを渡すと、「+」が「 」に置き換わっているため置換 */
$receipt = $this->replaceSpaceForPostData($receipt);
$decodedSignature = base64_decode($this->replaceSpaceForPostData($signature));

$result = (int)openssl_verify($receipt, $decodedSignature, $publicKey);

openssl_free_key($publicKey);

if ($result <= 0) {
return false;
}

return true;
}

/**
* RSA(PEM形式)公開鍵の取得
*
* @return string RSA(PEM形式)公開鍵
*/
private function pemPublicKey()
{
return "-----BEGIN PUBLIC KEY-----" . PHP_EOL .
chunk_split(PUBLIC_KEY, BASE64, PHP_EOL) .
"-----END PUBLIC KEY-----";
}

/**
* POSTデータのスペース置換
*
* @param string $data POSTデータ
*
* @return string POSTデータ
*/
private function replaceSpaceForPostData($data)
{
return str_replace(' ', '+', $data);
}

有効期限取得

Google Play Developer APIを使って、定期購入の有効期限を取得します。ライブラリを使用することもできますので、必要に応じて使用してください。「API」と「アプリ」の連携を行い、認証情報の設定を行います。下記あたりを参考にすると良いかと思います。リフレッシュトークンの取得はアプリ連携が終わった後の方がいいかも!?しれないです。アプリとの連携前にリフレッシュトークンは取得済みだったのですが、それを使ってもなかなか有効期限が取得できず、再取得したらできるようになりました。(この辺りの手順どこにあったか、見逃したかもしれないです。)

APIにアクセスして、有効期限を取得します。APIのアクセス数には制限がありますので、注意して実装します。例えば、1度購入情報の検証が全体通して問題がなかった場合、サーバーからアプリに有効期限情報を送信し、有効期限内は問い合わせを行わないなどの考慮をすると良いかと思います。毎回アプリの起動(や課金コンテンツを開く)タイミングで問い合わせてたらあっさり超えてしまいそうなので、工夫が必要です。実際の実装の際にもエラー処理を考慮した上で、サーバーへの問い合わせをどの状態の時に行うかを考え、実装するのが非常に苦労しました。APIからの情報取得は下記を参考にすることができます。

最後に課金情報をDBに保存したり、有効期限をアプリに送信して、アプリ側で購入完了処理をしたりすることで購入が完了となります。

まとめ

今回はAndroidの定期購入についてアプリ側からサーバー側までの一連の流れを書きました。定期購入と都度課金ではまた少し異なる部分があるので、その部分は注意していただければと思います。課金のバージョンが更新されるとまた別の方法になってしまうと思いますので、常に現在のものと同じバージョンで、正しいものなのか確認が必要かと思います。Android実装歴1年未満ですが、今回こちらの実装を行うことで、Androidの知識をまた1つ増やすことができ良かったと思います。様々な人に手助けをしていただきながら、開発を行うことができましたので、この記事がどなたかの手助けとなれれば幸いです。今後はE・レシピの有料・無料のコンテンツを増やしていき、より良いアプリ・サービスを目指していきたいと思います。

参考

まだまだ続くAdvent Calender、明日もお楽しみに!

エンジニア募集

エキサイトではエンジニアとして一緒に働いてくださる方を
新卒採用中途採用で募集しています。
詳しくは、こちらの採用情報ページをご覧ください。

Android ロボットは、Google が作成および提供している作品から複製または変更したものであり、Creative Commons 3.0 Attribution ライセンスに記載された条件に従って使用しています。

by ex-engineer | 2016-12-21 11:00