完全なプロセスのGooglePayアクセス



Complete Process Google Pay Access



しばらく前、プロジェクトの要件により、製品はGoogle Pay SDKにアクセスする必要があり、その後...誰もが知っているように...さまざまな検索、表示された記事は昔のものか、さまざまな問題がありました、しばらくして「泥の中を転がった後」、私は黙って公式チュートリアルドキュメントを選びました。まあ、公式デモは直接使用できません。いくつかの変更の後、最終テストに合格しました。現在オンラインで使用されています...コード内詳細なメモがあります。わからない場合は、コメント欄にメッセージを残してください。ブログを書くのは初めてです。ご不明な点がございましたら、お気軽にお問い合わせください。

コードをアップロード



1つ目は、購入管理クラスBillingManagerです。

package com.example.googleiap import android.app.Activity import android.support.annotation.Nullable import android.util.Log import com.android.billingclient.api.AcknowledgePurchaseParams import com.android.billingclient.api.AcknowledgePurchaseResponseListener import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingClient.BillingResponseCode import com.android.billingclient.api.BillingClient.SkuType import com.android.billingclient.api.BillingClient.FeatureType import com.android.billingclient.api.BillingClientStateListener import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingResult import com.android.billingclient.api.ConsumeParams import com.android.billingclient.api.ConsumeResponseListener import com.android.billingclient.api.Purchase import com.android.billingclient.api.Purchase.PurchasesResult 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.io.IOError import java.io.IOException import java.util.ArrayList import java.util.HashSet import java.util.List import java.util.Set public class BillingManager implements PurchasesUpdatedListener{ private static final String TAG = 'BillingManager' /*Purchase key*/ private static final String BASE_64_ENCODED_PUBLIC_KEY = 'Fill in here a string of Base64 secret keys generated by your Google backend' /*Uninitialized tag*/ public static final int BILLING_MANAGER_NOT_INITIALIZED = -1 /*Client*/ private BillingClient billingClient /*Activity*/ private final Activity mActivity /*Monitor*/ private final BillingUpdatesListener mBillingUpdatesListener /*Whether the connection is successful*/ private boolean mIsServiceConnected /*Current status of the client*/ private @BillingResponseCode int curBillingClientResponseCode = BillingResponseCode.SERVICE_DISCONNECTED /*Product list*/ private final List PurchaseList = new ArrayList() /*Consumption token*/ private Set mTokensToBeConsumed /*Monitor interface*/ public interface BillingUpdatesListener{ void onBillingClientSetupFinished() void onConsumeFinished(String token, @BillingResponseCode int result) void onPurchasesUpdated(List purchases) void onFailedHandle(@BillingResponseCode int result) } public BillingManager(Activity activity,final BillingUpdatesListener updatesListener){ Log.d(TAG, 'Create Billing Client') mActivity = activity mBillingUpdatesListener = updatesListener billingClient = BillingClient.newBuilder(mActivity).enablePendingPurchases().setListener(this).build() Log.d(TAG, 'Start setting information') startServiceConnection(new Runnable() { @Override public void run() { mBillingUpdatesListener.onBillingClientSetupFinished() Log.d(TAG, 'Set up the client successfully, start requesting product inventory') OnQueryPurchases() } }) } /*Start to connect to Play*/ public void startServiceConnection(final Runnable executeOnSuccess){ billingClient.startConnection(new BillingClientStateListener() { @Override public void onBillingSetupFinished(BillingResult billingResult) { Log.d(TAG, 'Setup finished. Response code: ' + billingResult) if(billingResult.getResponseCode() == BillingResponseCode.OK){ mIsServiceConnected = true if(executeOnSuccess != null){ executeOnSuccess.run() } } curBillingClientResponseCode = billingResult.getResponseCode() } @Override public void onBillingServiceDisconnected() { mIsServiceConnected = false } }) } /*Request product inventory*/ public void OnQueryPurchases() { Runnable queryToExecute = new Runnable() { @Override public void run() { //System current time long time = System.currentTimeMillis() //Request for in-app purchase PurchasesResult purchasesResult = billingClient.queryPurchases(SkuType.INAPP) Log.i(TAG, 'Request for in-app purchase of goods to spend time:' + (System.currentTimeMillis()-time) + 'ms') //Support subscription if(areSubscriptionsSupported()){ PurchasesResult subscriptionResult = billingClient.queryPurchases(SkuType.SUBS) Log.i(TAG, 'The time spent after requesting to subscribe to the product: ' + (System.currentTimeMillis() - time) + 'ms') if(subscriptionResult.getResponseCode() == BillingResponseCode.OK){ Log.i(TAG, 'Request subscription message return Code: ' + subscriptionResult.getResponseCode() + ' res: ' + subscriptionResult.getPurchasesList().size()) purchasesResult.getPurchasesList().addAll(subscriptionResult.getPurchasesList()) } else { Log.e(TAG, 'Please refer to Code for failure to obtain subscription product') } } else if (purchasesResult.getResponseCode() == BillingResponseCode.OK){ Log.i(TAG, 'Skip the request to subscribe to the product because the device does not support it') } else{ Log.w(TAG, 'Failed to request goods, return: ' + purchasesResult.getResponseCode()) } onQueryPurchasesFinished(purchasesResult) } } executeServiceRequest(queryToExecute) } /*Do you support subscription*/ public boolean areSubscriptionsSupported(){ int responseCode = billingClient.isFeatureSupported(FeatureType.SUBSCRIPTIONS).getResponseCode() if(responseCode != BillingResponseCode.OK) { Log.w(TAG, 'areSubscriptionsSupported() got an error response: ' + responseCode) } return responseCode == BillingResponseCode.OK } /*Request product information completed*/ private void onQueryPurchasesFinished(PurchasesResult result){ if(billingClient == null || result.getResponseCode() != BillingResponseCode.OK){ Log.w(TAG, 'billingClient is null or result code (' + result.getResponseCode() + ') was bad - quitting') return } Log.d(TAG, 'Request product information completed') PurchaseList.clear() onPurchasesUpdated(result.getBillingResult(),result.getPurchasesList()) } /*Update product*/ @Override public void onPurchasesUpdated(BillingResult billingResult,List purchases){ if(billingResult.getResponseCode() == BillingResponseCode.OK){ for (Purchase purchase : purchases){ HandlePurchase(purchase) } mBillingUpdatesListener.onPurchasesUpdated(PurchaseList) } else{ if(billingResult.getResponseCode() == BillingResponseCode.USER_CANCELED){ Log.i(TAG, 'onPurchasesUpdated()-the user canceled the purchase of the current product') } else{ Log.w(TAG, 'onPurchasesUpdated() got unknown resultCode: ' + billingResult.getResponseCode()) } mBillingUpdatesListener.onFailedHandle(billingResult.getResponseCode()) } } /*Product handling*/ private void HandlePurchase(Purchase purchase){ //Verify signature data Log.i(TAG,'getSignature => '+ purchase.getSignature()) if(!VerifyValidSignature(purchase.getOriginalJson(),purchase.getSignature())){ Log.i(TAG, 'Got a purchase: ' + purchase + ' but signature is bad. Skipping...') return } Log.d(TAG, 'Got a verified purchase: ' + purchase) PurchaseList.add(purchase) } /*Verify signature*/ private boolean VerifyValidSignature(String signedData,String signature){ try{ return Security.verifyPurchase(BASE_64_ENCODED_PUBLIC_KEY,signedData,signature) } catch (IOException e){ Log.e(TAG, 'Got an exception trying to validate a purchase: ' + e) return false } } /*Execute service request*/ private void executeServiceRequest(Runnable runnable) { if(mIsServiceConnected){ runnable.run() } else{ startServiceConnection(runnable) } } public void consumeAsync(final String purchaseToken) { if (mTokensToBeConsumed == null) { mTokensToBeConsumed = new HashSet() } else if (mTokensToBeConsumed.contains(purchaseToken)) { Log.i(TAG, 'Token was already scheduled to be consumed - skipping...') return } mTokensToBeConsumed.add(purchaseToken) //Consumption monitoring final ConsumeResponseListener onConsumeListener = new ConsumeResponseListener() { @Override public void onConsumeResponse(BillingResult responseCode, String purchaseToken) { // If billing service was disconnected, we try to reconnect 1 time // (feel free to introduce your retry policy here). mBillingUpdatesListener.onConsumeFinished(purchaseToken, responseCode.getResponseCode()) } } final ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchaseToken).build() Runnable consumeRequest = new Runnable() { @Override public void run() { // Consume the purchase async billingClient.consumeAsync(consumeParams, onConsumeListener) } } executeServiceRequest(consumeRequest) } /*Query in-app purchase product details*/ public void querySkuDetailsAsync(@SkuType final String itemType, final List skuList, final SkuDetailsResponseListener listener) { Runnable queryRequest = new Runnable(){ @Override public void run() { SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder() params.setSkusList(skuList).setType(itemType) billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() { @Override public void onSkuDetailsResponse(BillingResult billingResult, List skuDetailsList) { listener.onSkuDetailsResponse(billingResult, skuDetailsList) } }) } } executeServiceRequest(queryRequest) } // /*Start a purchase process*/ // public void initiatePurchaseFlow(final SkuDetails skuDetails, final @SkuType String billingType) { // initiatePurchaseFlow(skuDetails) // } /*Start purchase, order process*/ public void initiatePurchaseFlow(final SkuDetails skuDetails) { Runnable purchaseFlowRequest = new Runnable() { @Override public void run() { // Log.d(TAG, 'Launching in-app purchase flow. Replace old SKU? ' + (oldSkus != null)) BillingFlowParams purchaseParams = BillingFlowParams.newBuilder() .setSkuDetails(skuDetails).build() billingClient.launchBillingFlow(mActivity, purchaseParams) } } executeServiceRequest(purchaseFlowRequest) } public void acknowledgePurchase(AcknowledgePurchaseParams acknowledgePurchaseParams, AcknowledgePurchaseResponseListener Listener){ billingClient.acknowledgePurchase(acknowledgePurchaseParams,Listener) } /* Release the connection*/ public void destroy(){ Log.d(TAG, 'Destroying the manager.') if (billingClient != null && billingClient.isReady()) { billingClient.endConnection() billingClient = null } } } Then there is a product verification class package com.example.googleiap import android.text.TextUtils import android.util.Base64 import android.util.Log import com.android.billingclient.util.BillingHelper import java.io.IOException import java.security.InvalidKeyException import java.security.KeyFactory import java.security.NoSuchAlgorithmException import java.security.PublicKey import java.security.Signature import java.security.SignatureException import java.security.spec.InvalidKeySpecException import java.security.spec.X509EncodedKeySpec public class Security { private static final String TAG = 'GoogleIap/Security' private static final String KEY_FACTORY_ALGORITHM = 'RSA' private static final String SIGNATURE_ALGORITHM = 'SHA1withRSA' public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) throws IOException { if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) || TextUtils.isEmpty(signature)) { Log.w(TAG,'Purchase verification failed, data loss') return false } PublicKey key = generatePublicKey(base64PublicKey) return verify(key, signedData, signature) } public static PublicKey generatePublicKey(String encodedPublicKey) throws IOException { try { byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT) KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM) return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)) } catch (NoSuchAlgorithmException e) { // 'RSA' is guaranteed to be available. throw new RuntimeException(e) } catch (InvalidKeySpecException e) { String msg = 'Invalid key specification: ' + e BillingHelper.logWarn(TAG, msg) throw new IOException(msg) } } public static boolean verify(PublicKey publicKey, String signedData, String signature) { byte[] signatureBytes try { signatureBytes = Base64.decode(signature, Base64.DEFAULT) } catch (IllegalArgumentException e) { BillingHelper.logWarn(TAG, 'Base64 decoding failed.') return false } try { Signature signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM) signatureAlgorithm.initVerify(publicKey) signatureAlgorithm.update(signedData.getBytes()) if (!signatureAlgorithm.verify(signatureBytes)) { BillingHelper.logWarn(TAG, 'Signature verification failed.') return false } return true } catch (NoSuchAlgorithmException e) { // 'RSA' is guaranteed to be available. throw new RuntimeException(e) } catch (InvalidKeyException e) { BillingHelper.logWarn(TAG, 'Invalid key specification.') } catch (SignatureException e) { BillingHelper.logWarn(TAG, 'Signature exception.') } return false } }

BillingManagerとSecurityを別々のライブラリファイルとして使用して、将来のプロジェクトの移行を容易にします



それから使用方法があります

このカテゴリは、商品ロジックを処理せず、転送ステーションとしてのみ機能します。以下は、使用されるロジックの一部です

継承するクラスをAndroidプロジェクトに作成します



extends UnityPlayerActivity implements BillingManager.BillingUpdatesListener

使用されるメンバー属性

private static Dictionary ProductList = new Hashtable() /* Commodity token*/ private static Dictionary m_PurchaseListByToken = new Hashtable() /*Commodity*/ private static Dictionary m_PurchaseListBySku = new Hashtable() /*Is the in-app purchase completed?*/ private boolean bSetupFinis /*Is the product initialization completed?*/ private boolean bInitProduct private final String Buy = 'Buy' private final String Consume = 'Consume' private final String Success = 'Success' /*In-app purchase client*/ private BillingManager m_BillingManager

次に、プロジェクトで必要な場所でBillingManagerを初期化します。私のプロジェクトは、ロード後にゲームを初期化することです。

/*Initialization*/ public void InitBilling(String nil){ Log.w(TAG,'InitBilling') m_BillingManager = new BillingManager(this,this) } /*Client setting success callback*/ @Override public void onBillingClientSetupFinished() { bSetupFinis = true Log.i(TAG, 'onBillingClientSetupFinished') BeginQuestProuect() }

次に、製品リストを初期化します(呼び出すことができると思うときに呼び出すだけです)。一部の人々は、私がGoogleのバックグラウンドで製品リストを構成したと思いますか?なぜ製品を初期化したいのですか?これは、購入に必要な検証データを取得する必要があるため、一度要求し、ローカルの製品構成IDを記号でフォーマットしてから、返された製品情報ProductList .put(skuDetail.getSku()、skuDetail)を保存する必要があるためです。

public void InitProduceList(String productid){ String[] sArray = productid.split('#') for(String sku : sArray){ ProductKeys.add(sku) } bInitProduct = true BeginQuestProuect() } /*Ready to request product information*/ private void BeginQuestProuect(){ if(!bInitProduct || !bSetupFinis){ return } /*I am here to register once for the convenience of illustration*/ addProduct(ProductKeys,SkuType.INAPP) addProduct(ProductKeys,SkuType.SUBS) } /*Adding goods*/ public void addProduct(List skusList, final @SkuType String billingType) { m_BillingManager.querySkuDetailsAsync(billingType, skusList, new SkuDetailsResponseListener() { @Override public void onSkuDetailsResponse(BillingResult billingResult, List skuDetailsList) { if (billingResult.getResponseCode() != BillingResponseCode.OK) { Log.w(TAG, 'Unsuccessful query for type: ' + billingType + '. Error code: ' + billingResult.getResponseCode()) }else if(skuDetailsList != null && skuDetailsList.size() > 0){ for(SkuDetails skuDetail : skuDetailsList){ Log.i(TAG, 'Adding sku: ' + skuDetail) /*Save the returned product information*/ ProductList.put(skuDetail.getSku(),skuDetail) } } } }) }

製品アップデートのコールバック。アプリ内購入が初めて初期化されたときに、一部の製品情報が返されます。ここでは、購入してアクティブ化したままにし(これは、製品が確認されることを意味します)、購入は成功します。区別は購入または初期化によって引き起こされますか?商品の購入は一度に1つずつリクエストする必要があるため、すでに商品のカテゴリを購入している場合は、この購入の終了を待って次の購入を開始する必要があります(購入リクエストがあるかどうかがわかります)。

/*Product update callback*/ @Override public void onPurchasesUpdated(List purchases) { UnitySendMessage('State.0') String parame for (Purchase purchase : purchases){ if (purchase.getPurchaseState() == PurchaseState.PURCHASED || purchase.isAcknowledged()) { m_PurchaseListByToken.put(purchase.getPurchaseToken(),purchase) m_PurchaseListBySku.put(purchase.getSku(),purchase) parame = String.format('%s.%s.0.%s',Buy,purchase.getSku(),purchase.getPurchaseTime() / 1000) UnitySendMessage(parame) } } UnitySendMessage('State.1') Log.i(TAG, 'onPurchasesUpdated') } /*Failure handling*/ public void onFailedHandle(@BillingResponseCode int result){ String code = String.format('%s.-1.%s.0',Buy,result) Log.i(TAG, 'onFailedHandle code = ' + code) UnitySendMessage(code) }

次に、製品を購入します。これが初期化に必要な製品データです

/*Purchase goods*/ public void BuyProduct(String product){ Log.d(TAG,'BuyProduct:' + product) SkuDetails skuDetails= ProductList.get(product) if(skuDetails != null){ m_BillingManager.initiatePurchaseFlow(skuDetails) }else{ Log.w(TAG, 'not found product in ProductList ,product=> ' + product) } }

それでは商品の消費ですので、こちらにご注意ください! ! !グーグルの背景は、消費製品と非消費製品を区別していません。自分で管理し、構成で判断する必要があります

/*Consumable goods*/ public void consumeAsync(String product,String type){ // type = '2' Log.i(TAG,'consumeAsync product = ' + product + ',type = ' + type) Purchase purchase = m_PurchaseListBySku.get(product) if(purchase != null){ m_PurchaseListBySku.remove(product) if(type.equals('1')){ //Consumables m_BillingManager.consumeAsync(purchase.getPurchaseToken()) } else if (type.equals('0')){ if(!purchase.isAcknowledged()){ Log.i(TAG,'Go to confirm the product') AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.getPurchaseToken()) .build() m_BillingManager.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() { @Override public void onAcknowledgePurchaseResponse(BillingResult billingResult) { Log.i(TAG,'Go to confirm the product return code = '+billingResult.getResponseCode() ) String parame = String.format('%s.-1.%s.-1',Consume,billingResult.getResponseCode()) UnitySendMessage(parame) } }) } else{ String parame = String.format('%s.-1.0.-1',Consume) UnitySendMessage(parame) } } } }

商品消費完了

/*Commodity consumption completed callback*/ @Override public void onConsumeFinished(String token, @BillingResponseCode int result) { Purchase purchases = m_PurchaseListByToken.get(token) Log.i(TAG, 'onConsumeFinished (purchases)=>' + purchases) if(purchases != null){ String code = String.format('%s.%s.%s.%s',Consume,purchases.getSku(),result,purchases.getPurchaseTime()) UnitySendMessage(code) m_PurchaseListByToken.remove(purchases.getPurchaseToken()) } }

その後、バックグラウンドが切り替わったときに製品アップデートを呼び出す必要があります。これは、プレーヤーがGoogleストアで購入したゲーム製品である可能性があるためです。

@Override protected void onResume(){ super.onResume() if(m_BillingManager != null){ m_BillingManager.OnQueryPurchases() } }