谷歌支付總結(下)

[html] view plain copy
  1. 開篇:  
[html] view plain copy
  1. 如billing開發文檔所說,要在你的應用中實現In-app Billing只需要完成以下幾步就可以了。  




[html] view plain copy
  1. 第一,把你上篇下載的AIDL文件添加到你的工程裏,第二,把<uses-permission android:name="com.android.vending.BILLING" />  
這個權限加到你工程的AndroidManifest.xml文件中,第三,創建一個ServiceConnection,並把它綁定到IInAppBillingService中。完成上面三條後就可以使用支付了。當然這只是一個簡單的介紹。其實Google的這個支付,大部分都是你手機上的Google Play來進行處理的,你只需要處理購買請求,處理購買結果就行了。文檔寫的很好,先把這個文檔看完,就知道支付流程了。

正文:

1.內購商品相關

針對我的項目而言,我們在Google後臺設置的是受管理可消耗的商品("managed per user account"),具體的就是遊戲裏的水晶,玩家可以多次購買。但是Google後臺的這種可重複購買商品(還有一種是隻能購買一次的商品"subscription")有個要求,就是你購買成功後需要主動向Google Play請求消耗這個商品,等消耗成功後你纔可以再次下單購買。因此,在遊戲裏的支付會多出一個操作步驟就是請求消耗購買成功的商品。

2.檢測設備是否支持Google Play Service

在正式開啓支付前,Google billing會檢查你的手機是否支持Google billing,這個下面會講到。爲了更好的用戶體驗,建議在Google billing檢測之前,可以先檢測一下用戶的設備是否支持Google Play Service,其中基本要求就是安裝了Google Play應用商店和Google Play Service。如果用戶的設備不具備這兩個,就可以彈出提示引導用戶去安裝。這裏有兩種方式可以用,一種是通過Google Play Service來進行檢測,就是上篇下載的那個Service擴展包,一種是自己寫代碼,遍歷設備上安裝的應用程序,檢查是否有安裝Google Play。先說第一種。

(1)Google Play Service

上篇下載的Service包裏會有一個庫工程

把這個庫工程導入你的eclipse,引用到你的工程裏就可以用了,具體操作可以參加docs下的文檔,so easy!導入成功後,調用其中的一個方法就可以了。

[html] view plain copy
  1. /**  
  2.  * Check the device to make sure it has the Google Play Services APK.If  
  3.  * it doesn't, display a dialog that allows users to download the APK from  
  4.  * the Google Play Store or enable it in the device's system settings  
  5.  */  
  6. private boolean checkPlayServices()  
  7. {  
  8.     int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);  
  9.     if(resultCode != ConnectionResult.SUCCESS)  
  10.     {  
  11.         if(GooglePlayServicesUtil.isUserRecoverableError(resultCode))  
  12.         {  
  13.             GooglePlayServicesUtil.getErrorDialog(resultCode, this,  
  14.                     PLAY_SERVICES_RESOLUTION_REQUEST).show();  
  15.         }  
  16.         else  
  17.         {  
  18.             Log.i(TAG, "This device is not supported");  
  19.             finish();  
  20.         }  
  21.         return false;  
  22.     }  
  23.     return true;  
  24. }  
如果當前設備的Google Service不可用,就會彈出提示,引導用戶去設置安裝。如果此設備不支持的話,就也不需要檢測Google billing是否可用了。多說一句,Google Play Service可以做很多事的,如果覺得只用上面的功能太簡單的話,就可以考慮把應用自動更新也加上,當你在Google Play上傳了新版程序後,Google Play會幫你提示用戶更新程序。還有一個比較好玩的就是如果引入了這個庫工程後,就可以加GCM了(Google Cloud Messaging),就是消息推送推送功能,當然這個比較麻煩,有興趣的可以去加加看。

(2)遍歷包名

Google Play的程序包名是"com.Android.vending",運行在設備上的Google Play Service的包名是"com.google.android.gms",可以在程序啓動的時候遍歷下設備上的包名,如果沒有這兩個東西就引導用戶去安裝。

遍歷包名方法

[html] view plain copy
  1. //Check Google Play  
  2. protected boolean isHaveGooglePlay(Context context, String packageName)  
  3. {  
  4.     //Get PackageManager  
  5.     final PackageManager packageManager = context.getPackageManager();  
  6.   
  7.     //Get The All Install App Package Name  
  8.     List<PackageInfo> pInfo = packageManager.getInstalledPackages(0);  
  9.       
  10.     //Create Name List  
  11.     List<String> pName = new ArrayList<String>();  
  12.       
  13.     //Add Package Name into Name List  
  14.     if(pInfo != null){  
  15.         for(int i=0; i<pInfo.size(); i++){  
  16.             String pn = pInfo.get(i).packageName;  
  17.             pName.add(pn);  
  18.               
  19.             //Log.v("Package Name", "PackAgeName: = " + pn);  
  20.         }  
  21.     }  
  22.       
  23.     //Check   
  24.     return pName.contains(packageName);  
  25. }  
提示安裝方法

[html] view plain copy
  1. Uri uri = Uri.parse("market://details?id=" + "要安裝程序的包名");  
  2.             Intent it = new Intent(Intent.ACTION_VIEW, uri);   
  3.             startActivity(it);  
上面這個方法會打開你手機上的應用商店,定位到要安裝的程序。

不過還是推薦用Google Play Service來檢測,貌似第二種,即使有的用戶裝了Google Play(像國內用戶),也不支持Google Play Service的。3.添加代碼(終於要加支付代碼了)

把上篇下載的samples裏util的代碼全部拷到你的工程裏,可以新建一個包,放到裏面。

這個說明一下,其實這個例子的代碼還是不錯的,本着天下代碼一大抄和拿來主義,就直接拿來用吧!當然如果你覺得這個代碼寫的不好,或者不適用你的工程,你就可以依據文檔自己寫適用的代碼。當然文檔裏說過,爲了防止別人破解你的遊戲,最好把裏面的變量和方法都改下名字,畢竟這裏的代碼任何人都看得到。我的做法是照搬過來了,只是把IabHelper.Java改造了下,因爲這個是整個支付的關鍵,其他都是輔助的,可以不管。

把這裏的代碼拷完,把該import的都import了,你就可以照samples中的代碼開寫自己的支付了。針對單機遊戲,就需要考慮這個代碼改造和本地的驗證,加密了。針對網絡遊戲就要簡單了。因爲我其實對java不太熟悉吐舌頭,所以單機的加密,base驗證,混淆什麼的就不做介紹了。下面主要說網絡遊戲。

(1)IabHelper.java

這個是支付的關鍵代碼,其中已經把設置billing,商品查詢,商品購買,商品回調,商品驗證以及回調方法都寫好了,你直接參照samples用就可以了。

01.設置billing

就是開篇所說的綁定ServiceConnection到IInAppBillingService。功能很完善,包括成功和失敗都有回調,還有各種異常。在你程序的啓動Activity裏檢測完設備是否Google Play Service後,就可以new一個IabHelper,來調用這個方法,根據不同的回調裏做相應的處理。

[html] view plain copy
  1. /**  
  2.      * Starts the setup process. This will start up the setup process asynchronously.  
  3.      * You will be notified through the listener when the setup process is complete.  
  4.      * This method is safe to call from a UI thread.  
  5.      *  
  6.      * @param listener The listener to notify when the setup process is complete.  
  7.      */  
  8.     public void startSetup(final OnIabSetupFinishedListener listener) {  
  9.         // If already set up, can't do it again.  
  10.         checkNotDisposed();  
  11.         if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");  
  12.   
  13.         // Connection to IAB service  
  14.         logDebug("Starting in-app billing setup.");  
  15.         mServiceConn = new ServiceConnection() {  
  16.             @Override  
  17.             public void onServiceDisconnected(ComponentName name) {  
  18.                 logDebug("Billing service disconnected.");  
  19.                 mService = null;  
  20.             }  
  21.   
  22.             @Override  
  23.             public void onServiceConnected(ComponentName name, IBinder service) {  
  24.                 if (mDisposed) return;  
  25.                 logDebug("Billing service connected.");  
  26.                 mService = IInAppBillingService.Stub.asInterface(service);  
  27.                 String packageName = mContext.getPackageName();  
  28.                 try {  
  29.                     logDebug("Checking for in-app billing 3 support.");  
  30.   
  31.                     // check for in-app billing v3 support  
  32.                     int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);  
  33.                     if (response != BILLING_RESPONSE_RESULT_OK) {  
  34.                         if (listener != null) listener.onIabSetupFinished(new IabResult(response,  
  35.                                 "Error checking for billing v3 support."));  
  36.   
  37.                         // if in-app purchases aren't supported, neither are subscriptions.  
  38.                         mSubscriptionsSupported = false;  
  39.                         return;  
  40.                     }  
  41.                     logDebug("In-app billing version 3 supported for " + packageName);  
  42.   
  43.                     // check for v3 subscriptions support  
  44.                     response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);  
  45.                     if (response == BILLING_RESPONSE_RESULT_OK) {  
  46.                         logDebug("Subscriptions AVAILABLE.");  
  47.                         mSubscriptionsSupported = true;  
  48.                     }  
  49.                     else {  
  50.                         logDebug("Subscriptions NOT AVAILABLE. Response: " + response);  
  51.                     }  
  52.   
  53.                     mSetupDone = true;  
  54.                 }  
  55.                 catch (RemoteException e) {  
  56.                     if (listener != null) {  
  57.                         listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,  
  58.                                                     "RemoteException while setting up in-app billing."));  
  59.                     }  
  60.                     e.printStackTrace();  
  61.                     return;  
  62.                 }  
  63.   
  64.                 if (listener != null) {  
  65.                     listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));  
  66.                 }  
  67.             }  
  68.         };  
  69.   
  70.         Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");  
  71.         serviceIntent.setPackage("com.android.vending");  
  72.         if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {  
  73.             // service available to handle that Intent  
  74.             mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);  
  75.         }  
  76.         else {  
  77.             // no service available to handle that Intent  
  78.             if (listener != null) {  
  79.                 listener.onIabSetupFinished(  
  80.                         new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,  
  81.                         "Billing service unavailable on device."));  
  82.             }  
  83.         }  
  84.     }  
[html] view plain copy
  1. samples中的代碼  
[html] view plain copy
  1. // Create the helper, passing it our context and the public key to verify signatures with  
  2.         Log.d(TAG, "Creating IAB helper.");  
  3.         mHelper = new IabHelper(this, base64EncodedPublicKey);  
  4.   
  5.   
  6.         // enable debug logging (for a production application, you should set this to false).  
  7.         mHelper.enableDebugLogging(true);  
  8.   
  9.   
  10.         // Start setup. This is asynchronous and the specified listener  
  11.         // will be called once setup completes.  
  12.         Log.d(TAG, "Starting setup.");  
  13.         mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {  
  14.             public void onIabSetupFinished(IabResult result) {  
  15.                 Log.d(TAG, "Setup finished.");  
  16.   
  17.   
  18.                 if (!result.isSuccess()) {  
  19.                     // Oh noes, there was a problem.  
  20.                     complain("Problem setting up in-app billing: " + result);  
  21.                     return;  
  22.                 }  
  23.   
  24.   
  25.                 // Have we been disposed of in the meantime? If so, quit.  
  26.                 if (mHelper == null) return;  
  27.   
  28.   
  29.                 // IAB is fully set up. Now, let's get an inventory of stuff we own.  
  30.                 Log.d(TAG, "Setup successful. Querying inventory.");  
  31.                 mHelper.queryInventoryAsync(mGotInventoryListener);  
  32.             }  
  33.         });  
  34.     }  

02.查詢商品

在setup方法的最後有一個

[html] view plain copy
  1. mHelper.queryInventoryAsync(mGotInventoryListener);  
是用來查詢你目前擁有的商品的。其中的回調的代碼如下

[html] view plain copy
  1. // Listener that's called when we finish querying the items and subscriptions we own  
  2.     IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {  
  3.         public void onQueryInventoryFinished(IabResult result, Inventory inventory) {  
  4.             Log.d(TAG, "Query inventory finished.");  
  5.   
  6.             // Have we been disposed of in the meantime? If so, quit.  
  7.             if (mHelper == null) return;  
  8.   
  9.             // Is it a failure?  
  10.             if (result.isFailure()) {  
  11.                 complain("Failed to query inventory: " + result);  
  12.                 return;  
  13.             }  
  14.   
  15.             Log.d(TAG, "Query inventory was successful.");  
  16.   
  17.             /*  
  18.              * Check for items we own. Notice that for each purchase, we check  
  19.              * the developer payload to see if it's correct! See  
  20.              * verifyDeveloperPayload().  
  21.              */  
  22.   
  23.             // Do we have the premium upgrade?  
  24.             Purchase premiumPurchase = inventory.getPurchase(SKU_PREMIUM);  
  25.             mIsPremium = (premiumPurchase != null && verifyDeveloperPayload(premiumPurchase));  
  26.             Log.d(TAG, "User is " + (mIsPremium ? "PREMIUM" : "NOT PREMIUM"));  
  27.   
  28.             // Do we have the infinite gas plan?  
  29.             Purchase infiniteGasPurchase = inventory.getPurchase(SKU_INFINITE_GAS);  
  30.             mSubscribedToInfiniteGas = (infiniteGasPurchase != null &&  
  31.                     verifyDeveloperPayload(infiniteGasPurchase));  
  32.             Log.d(TAG, "User " + (mSubscribedToInfiniteGas ? "HAS" : "DOES NOT HAVE")  
  33.                         + " infinite gas subscription.");  
  34.             if (mSubscribedToInfiniteGas) mTank = TANK_MAX;  
  35.   
  36.             // Check for gas delivery -- if we own gas, we should fill up the tank immediately  
  37.             Purchase gasPurchase = inventory.getPurchase(SKU_GAS);  
  38.             if (gasPurchase != null && verifyDeveloperPayload(gasPurchase)) {  
  39.                 Log.d(TAG, "We have gas. Consuming it.");  
  40.                 mHelper.consumeAsync(inventory.getPurchase(SKU_GAS), mConsumeFinishedListener);  
  41.                 return;  
  42.             }  
  43.   
  44.             updateUi();  
  45.             setWaitScreen(false);  
  46.             Log.d(TAG, "Initial inventory query finished; enabling main UI.");  
  47.         }  
  48.     };  
因爲目前我們的內購商品是可重複購買的,所以在成功查詢到我們已經購買的商品後進行了消耗商品操作。消耗的代碼在這裏

[html] view plain copy
  1. // Check for gas delivery -- if we own gas, we should fill up the tank immediately  
  2. Purchase gasPurchase = inventory.getPurchase(SKU_GAS);  
  3. if (gasPurchase != null && verifyDeveloperPayload(gasPurchase)) {  
  4.     Log.d(TAG, "We have gas. Consuming it.");  
  5.     mHelper.consumeAsync(inventory.getPurchase(SKU_GAS), mConsumeFinishedListener);  
  6.     return;  
  7. }  
在講消耗前,先解釋下以上這麼操作的原因。在內購商品那裏講過,如果是設置的是可重複商品,當你在成功購買這個商品後是需要主動消耗的,只有消耗成功後纔可以再次購買。可能有些人覺得這種設定不好,我的商品本來就是可重複購買的,爲什麼還要在買成功後通知Google Play消耗掉商品呢(可能本身商品沒用消耗掉,這只是一種叫法)?我個人覺得這樣設定,第一,可以避免用戶重複下單購買,第二,可以保證每筆消費訂單的唯一。有了以上兩點就可以很好地處理漏單。 so,上面代碼在成功設置billing後,第一個操作就是查詢擁有的商品,就是做的漏單處理。因爲支付過程其實就是你的應用程序----->Google Play程序(通過Google Play Service)------>Google服務器------->Google Play程序(通過Google Play Service)------>你的應用程序。這樣一個交互過程,還需要網絡支持,所以每次支付操作不會保證百分百成功,這樣就會產生漏單現象,就是用戶付費成功了,但是Google Play在通知你的應用程序支付結果時,因爲某些原因斷掉了,這樣你的程序就不知道支付是否操作成功了,so,只好在下次進遊戲時查查有沒有已經購買的,但是還沒有消耗的商品,有的話就消耗掉,然後再把商品發送給用戶。因爲這個商品在消耗之前,用戶是無法再次購買的,所以單個用戶就只會對應單一的漏單,不會有重複的漏單。這些信息都是存到Google服務器上的,直接調代碼裏的查詢代碼就可以了。

02.消耗商品

消耗商品會在兩個地方出現。一,查詢商品中所說的漏單中,二,就是你每次購買成功後的消耗。消耗商品也有一個回調,如下

[html] view plain copy
  1. // Called when consumption is complete  
  2.    IabHelper.OnConsumeFinishedListener mConsumeFinishedListener = new IabHelper.OnConsumeFinishedListener() {  
  3.        public void onConsumeFinished(Purchase purchase, IabResult result) {  
  4.            Log.d(TAG, "Consumption finished. Purchase: " + purchase + ", result: " + result);  
  5.   
  6.            // if we were disposed of in the meantime, quit.  
  7.            if (mHelper == null) return;  
  8.   
  9.            // We know this is the "gas" sku because it's the only one we consume,  
  10.            // so we don't check which sku was consumed. If you have more than one  
  11.            // sku, you probably should check...  
  12.            if (result.isSuccess()) {  
  13.                // successfully consumed, so we apply the effects of the item in our  
  14.                // game world's logic, which in our case means filling the gas tank a bit  
  15.                Log.d(TAG, "Consumption successful. Provisioning.");  
  16.                mTank = mTank == TANK_MAX ? TANK_MAX : mTank + 1;  
  17.                saveData();  
  18.                alert("You filled 1/4 tank. Your tank is now " + String.valueOf(mTank) + "/4 full!");  
  19.            }  
  20.            else {  
  21.                complain("Error while consuming: " + result);  
  22.            }  
  23.            updateUi();  
  24.            setWaitScreen(false);  
  25.            Log.d(TAG, "End consumption flow.");  
  26.        }  
  27.    };  
代碼比較簡單,針對自己的遊戲邏輯,在裏面稍做改動即可。

03.購買商品

按重要程度,購買商品應該排在第一位的,只是按支付流程走的話,購買商品卻不是第一位,這裏就根據支付流程來走吧。

[html] view plain copy
  1. /**  
  2.     * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,  
  3.     * which will involve bringing up the Google Play screen. The calling activity will be paused while  
  4.     * the user interacts with Google Play, and the result will be delivered via the activity's  
  5.     * {@link android.app.Activity#onActivityResult} method, at which point you must call  
  6.     * this object's {@link #handleActivityResult} method to continue the purchase flow. This method  
  7.     * MUST be called from the UI thread of the Activity.  
  8.     *  
  9.     * @param act The calling activity.  
  10.     * @param sku The sku of the item to purchase.  
  11.     * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)  
  12.     * @param requestCode A request code (to differentiate from other responses --  
  13.     *     as in {@link android.app.Activity#startActivityForResult}).  
  14.     * @param listener The listener to notify when the purchase process finishes  
  15.     * @param extraData Extra data (developer payload), which will be returned with the purchase data  
  16.     *     when the purchase completes. This extra data will be permanently bound to that purchase  
  17.     *     and will always be returned when the purchase is queried.  
  18.     */  
  19.    public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,  
  20.                        OnIabPurchaseFinishedListener listener, String extraData) {  
  21.        checkNotDisposed();  
  22.        checkSetupDone("launchPurchaseFlow");  
  23.        flagStartAsync("launchPurchaseFlow");  
  24.        IabResult result;  
  25.   
  26.        if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {  
  27.            IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,  
  28.                    "Subscriptions are not available.");  
  29.            flagEndAsync();  
  30.            if (listener != null) listener.onIabPurchaseFinished(r, null);  
  31.            return;  
  32.        }  
  33.   
  34.        try {  
  35.            logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);  
  36.            Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);  
  37.            int response = getResponseCodeFromBundle(buyIntentBundle);  
  38.            if (response != BILLING_RESPONSE_RESULT_OK) {  
  39.                logError("Unable to buy item, Error response: " + getResponseDesc(response));  
  40.                flagEndAsync();  
  41.                result = new IabResult(response, "Unable to buy item");  
  42.                if (listener != null) listener.onIabPurchaseFinished(result, null);  
  43.                return;  
  44.            }  
  45.   
  46.            PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);  
  47.            logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);  
  48.            mRequestCode = requestCode;  
  49.            mPurchaseListener = listener;  
  50.            mPurchasingItemType = itemType;  
  51.            act.startIntentSenderForResult(pendingIntent.getIntentSender(),  
  52.                                           requestCode, new Intent(),  
  53.                                           Integer.valueOf(0), Integer.valueOf(0),  
  54.                                           Integer.valueOf(0));  
  55.        }  
  56.        catch (SendIntentException e) {  
  57.            logError("SendIntentException while launching purchase flow for sku " + sku);  
  58.            e.printStackTrace();  
  59.            flagEndAsync();  
  60.   
  61.            result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");  
  62.            if (listener != null) listener.onIabPurchaseFinished(result, null);  
  63.        }  
  64.        catch (RemoteException e) {  
  65.            logError("RemoteException while launching purchase flow for sku " + sku);  
  66.            e.printStackTrace();  
  67.            flagEndAsync();  
  68.   
  69.            result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");  
  70.            if (listener != null) listener.onIabPurchaseFinished(result, null);  
  71.        }  
  72.    }  
以上是IabHelper中的支付購買代碼,其中包括了重複購買商品類型和一次購買商品類型的處理。主要的代碼是try裏面的這一塊

[html] view plain copy
  1. try {  
  2.     logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);  
  3.     Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);  
  4.     int response = getResponseCodeFromBundle(buyIntentBundle);  
  5.     if (response != BILLING_RESPONSE_RESULT_OK) {  
  6.         logError("Unable to buy item, Error response: " + getResponseDesc(response));  
  7.         flagEndAsync();  
  8.         result = new IabResult(response, "Unable to buy item");  
  9.         if (listener != null) listener.onIabPurchaseFinished(result, null);  
  10.         return;  
  11.     }  
  12.   
  13.     PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);  
  14.     logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);  
  15.     mRequestCode = requestCode;  
  16.     mPurchaseListener = listener;  
  17.     mPurchasingItemType = itemType;  
  18.     act.startIntentSenderForResult(pendingIntent.getIntentSender(),  
  19.                                    requestCode, new Intent(),  
  20.                                    Integer.valueOf(0), Integer.valueOf(0),  
  21.                                    Integer.valueOf(0));  
  22. }  
一,調用In-app Billing中的getBuyIntent方法,會傳幾個參數,第一個參數 3 代表的是當前所用的支付API的版本,第二個參數是你的包名,第三個參數就是你內購商品的ID,第四個參數是這次購買的類型,“inapp”和"subs",我們用的是第一個,第二個是隻能購買一次的類型,第五個參數是訂單號。需要講的只有第三個和第五個參數。

第三個參數,商品Id,就是你在Google開發者後臺上設置的內購商品的名字。每個商品的名字要唯一。推薦用商品名字加下劃線加價格的組合,比如"crystal_0.99",這樣你一看名字就知道這個商品的價格是0.99美金,商品是水晶。

第五個參數,訂單號。如果本地有支付服務器的話,這個訂單號可以由支付服務器生成,然後再傳給客戶端,這樣的話本地服務器也可以記錄下訂單信息,方便以後的查詢和操作。訂單號的格式推薦用時間戳加商品名字和價格,這樣也可以容易看出訂單信息。這個訂單號會傳給Google,購買成功後Google會原樣傳給你,所以也可以在其中加個標示信息,可以做下比對。

二,在getBuyIntent成功後,返回的Bundle中會有個BILLING_RESPONSE_RESULT_OK返回碼,這就代表成功了。然後再用這個Bundle得到一個PendingIntent.如上面代碼演示。

三,進行支付

[html] view plain copy
  1. act.startIntentSenderForResult(pendingIntent.getIntentSender(),  
  2.                                requestCode, new Intent(),  
  3.                                Integer.valueOf(0), Integer.valueOf(0),  
  4.                                Integer.valueOf(0));  


這個方法是Activity中的一個方法,調用這個方法後,回有一個回調來接收結果。除了第一個PengdingIntent參數外,其他的可以按參數類型隨便寫。

四,支付完成

[html] view plain copy
  1. /**  
  2.     * Handles an activity result that's part of the purchase flow in in-app billing. If you  
  3.     * are calling {@link #launchPurchaseFlow}, then you must call this method from your  
  4.     * Activity's {@link android.app.Activity@onActivityResult} method. This method  
  5.     * MUST be called from the UI thread of the Activity.  
  6.     *  
  7.     * @param requestCode The requestCode as you received it.  
  8.     * @param resultCode The resultCode as you received it.  
  9.     * @param data The data (Intent) as you received it.  
  10.     * @return Returns true if the result was related to a purchase flow and was handled;  
  11.     *     false if the result was not related to a purchase, in which case you should  
  12.     *     handle it normally.  
  13.     */  
  14.    public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {  
  15.        IabResult result;  
  16.        if (requestCode != mRequestCode) return false;  
  17.   
  18.        checkNotDisposed();  
  19.        checkSetupDone("handleActivityResult");  
  20.   
  21.        // end of async purchase operation that started on launchPurchaseFlow  
  22.        flagEndAsync();  
  23.   
  24.        if (data == null) {  
  25.            logError("Null data in IAB activity result.");  
  26.            result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");  
  27.            if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);  
  28.            return true;  
  29.        }  
  30.   
  31.        int responseCode = getResponseCodeFromIntent(data);  
  32.        String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);  
  33.        String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);  
  34.   
  35.        if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {  
  36.            logDebug("Successful resultcode from purchase activity.");  
  37.            logDebug("Purchase data: " + purchaseData);  
  38.            logDebug("Data signature: " + dataSignature);  
  39.            logDebug("Extras: " + data.getExtras());  
  40.            logDebug("Expected item type: " + mPurchasingItemType);  
  41.   
  42.            if (purchaseData == null || dataSignature == null) {  
  43.                logError("BUG: either purchaseData or dataSignature is null.");  
  44.                logDebug("Extras: " + data.getExtras().toString());  
  45.                result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");  
  46.                if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);  
  47.                return true;  
  48.            }  
  49.   
  50.            Purchase purchase = null;  
  51.            try {  
  52.                purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);  
  53.                String sku = purchase.getSku();  
  54.   
  55.                // Verify signature  
  56.                if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {  
  57.                    logError("Purchase signature verification FAILED for sku " + sku);  
  58.                    result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);  
  59.                    if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase);  
  60.                    return true;  
  61.                }  
  62.                logDebug("Purchase signature successfully verified.");  
  63.            }  
  64.            catch (JSONException e) {  
  65.                logError("Failed to parse purchase data.");  
  66.                e.printStackTrace();  
  67.                result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");  
  68.                if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);  
  69.                return true;  
  70.            }  
  71.   
  72.            if (mPurchaseListener != null) {  
  73.                mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);  
  74.            }  
  75.        }  
  76.        else if (resultCode == Activity.RESULT_OK) {  
  77.            // result code was OK, but in-app billing response was not OK.  
  78.            logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));  
  79.            if (mPurchaseListener != null) {  
  80.                result = new IabResult(responseCode, "Problem purchashing item.");  
  81.                mPurchaseListener.onIabPurchaseFinished(result, null);  
  82.            }  
  83.        }  
  84.        else if (resultCode == Activity.RESULT_CANCELED) {  
  85.            logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));  
  86.            result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");  
  87.            if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);  
  88.        }  
  89.        else {  
  90.            logError("Purchase failed. Result code: " + Integer.toString(resultCode)  
  91.                    + ". Response: " + getResponseDesc(responseCode));  
  92.            result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");  
  93.            if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);  
  94.        }  
  95.        return true;  
  96.    }  
  97.   
  98.    public Inventory queryInventory(boolean querySkuDetails, List<String> moreSkus) throws IabException {  
  99.        return queryInventory(querySkuDetails, moreSkus, null);  
  100.    }  

支付結果返回後會調用上面這個方法,對於支付失敗和其中的錯誤,代碼寫的很清楚,可以自行處理。關於上面的這個方法,這裏簡單說一下流程,看看這個方法是從哪調用的。首先去Sample裏的MainActivity找到那個 onActivityResult 方法

[java] view plain copy
  1. protected void onActivityResult(int requestCode, int resultCode, Intent data) {  
  2.     Log.d(TAG, "onActivityResult(" + requestCode + "," + resultCode + "," + data);  
  3.     if (mHelper == nullreturn;  
  4.   
  5.     // Pass on the activity result to the helper for handling  
  6.     if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {  
  7.         // not handled, so handle it ourselves (here's where you'd  
  8.         // perform any handling of activity results not related to in-app  
  9.         // billing...  
  10.         super.onActivityResult(requestCode, resultCode, data);  
  11.     }  
  12.     else {  
  13.         Log.d(TAG, "onActivityResult handled by IABUtil.");  
  14.     }  
  15. }  
這個方法會在支付結束,你的程序重新回到前臺的時候調用。在這個方法中可以看到

[java] view plain copy
  1. !mHelper.handleActivityResult(requestCode, resultCode, data)  
這裏調用了 IabHelper 裏的 handleActivityResult 方法。然後再到此方法裏會看到調用 PurchseListener 的地方

[java] view plain copy
  1. if (mPurchaseListener != null) {  
  2.                 mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);  
  3.             }  

至此就可以知道 OnIabPurchaseFinishedListener 是從哪調用的了。然後在 handleActivityResult  方法裏還可以看到這段代碼

[java] view plain copy
  1. // Verify signature  
  2.                if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {  
  3.                    logError("Purchase signature verification FAILED for sku " + sku);  
  4.                    result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);  
  5.                    if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase);  
  6.                    return true;  
  7.                }  
  8.                logDebug("Purchase signature successfully verified.");  

也是有童鞋在問,我購買結束後,Google Play都提示購買成功了,但是在 OnIabPurchaseFinishedListener  卻還是失敗的,失敗的信息就是


Purchase signature verification FAILED for sku xxx


這個錯誤信息就是從這裏輸出的,至於爲何出現這個錯誤,就是因爲 Sample 裏的本地驗證失敗了(其實已經購買成功了)。出現這個比較多的情況就是因爲使用Google保留測試ID:

android.test.purchased

因爲在 Security.java 中 會驗證失敗

[java] view plain copy
  1. public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {  
  2.      if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||  
  3.              TextUtils.isEmpty(signature)) {  
  4.          Log.e(TAG, "Purchase verification failed: missing data.");  
  5.          return false;  
  6.      }  
  7.   
  8.      PublicKey key = Security.generatePublicKey(base64PublicKey);  
  9.      return Security.verify(key, signedData, signature);  
  10.  }  

或者是   base64PublicKey 爲空,又或者是 signature 是空,就會驗證失敗。

解決方法:

1.使用本地驗證。就去看看 Security 中的方法,然後仔細檢查看是哪裏出問題了。

2.使用服務器驗證。就去改造下 IabHelper 中的 handleActivityResult f方法,不再使用本地的 Security 做驗證,如何操作可以看下面的內容。


Sample裏的Security還有一個坑,就是在你成功購買商品後,但是沒有消耗,下次再登錄遊戲進行查詢的時候會報個錯誤

[java] view plain copy
  1. Failed to query inventory: IabResult: Error refreshing inventory (querying owned items). (response: -1003:Purchase signature verification failed)  

之所以出現這個錯誤,是因爲在查詢的時候,也會運行 Security 中的 verifyPurchase 方法。追本溯源,一步一步的查找代碼,會在 IabHelper 中的
[java] view plain copy
  1. int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {... ...}  

此方法中找到Security 調用 verifyPurchase 。


如果是服務器做驗證的話,就如上圖示,把驗證註釋掉,如果是本地驗證的話,就去查找 Security 中的  verifyPurchase 方法,看看哪裏出錯了,哪裏傳的值爲空。

 注意

把驗證代碼註釋掉的時候記得把生成 Purchase 的那兩行代碼提出來,否則你在查詢的時候不會返回查詢到的商品。仔細看Sample的代碼的話,你會發現其實很多回調監聽都是從IabHelper中調用的。兩行代碼如下:

[java] view plain copy
  1. Purchase purchase = new Purchase(itemType, purchaseData, signature);  
  2.                   
  3.                 inv.addPurchase(purchase);  

根據查詢到的數據生成一個 purchase,然後把這個purchase 加入到 Inventory中,這樣你就可以在查詢成功的時候通過調用 

inventory.getPurchase 方法來獲取已經購買但是未消耗的商品了。



現在來關注支付成功後的結果驗證。在上面方法中會從支付結果的數據中取得兩個json數據。

[html] view plain copy
  1. int responseCode = getResponseCodeFromIntent(data);  
  2. String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);  
  3. String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);  
就是purchaseData和dataSignature。驗證支付就是需要這兩個參數和publicKey,例子裏的驗證方法是寫在Security.java裏的。裏面寫了三個方法來完成支付結果的驗證。

對於有本地支付服務器的遊戲來說,這個操作就可以放到服務端了,客戶端只需要把purchaseData和dataSignature傳給支付服務器即可。然後有支付服務器把驗證結果傳給客戶端,再做成功和失敗的處理。成功後則要進行消耗商品的操作。對於沒有支付服務器的遊戲來說,我個人覺得本地的操作要達到安全,還是比較難的。不過對於服務器驗證支付結果,也是存在風險的,只是風險要小。

[html] view plain copy
  1. /**  
  2.     * Verifies that the data was signed with the given signature, and returns  
  3.     * the verified purchase. The data is in JSON format and signed  
  4.     * with a private key. The data also contains the {@link PurchaseState}  
  5.     * and product ID of the purchase.  
  6.     * @param base64PublicKey the base64-encoded public key to use for verifying.  
  7.     * @param signedData the signed JSON string (signed, not encrypted)  
  8.     * @param signature the signature for the data, signed with the private key  
  9.     */  
  10.    public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {  
  11.        if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||  
  12.                TextUtils.isEmpty(signature)) {  
  13.            Log.e(TAG, "Purchase verification failed: missing data.");  
  14.            return false;  
  15.        }  
  16.   
  17.        PublicKey key = Security.generatePublicKey(base64PublicKey);  
  18.        return Security.verify(key, signedData, signature);  
  19.    }  
  20.   
  21.    /**  
  22.     * Generates a PublicKey instance from a string containing the  
  23.     * Base64-encoded public key.  
  24.     *  
  25.     * @param encodedPublicKey Base64-encoded public key  
  26.     * @throws IllegalArgumentException if encodedPublicKey is invalid  
  27.     */  
  28.    public static PublicKey generatePublicKey(String encodedPublicKey) {  
  29.        try {  
  30.            byte[] decodedKey = Base64.decode(encodedPublicKey);  
  31.            KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);  
  32.            return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));  
  33.        } catch (NoSuchAlgorithmException e) {  
  34.            throw new RuntimeException(e);  
  35.        } catch (InvalidKeySpecException e) {  
  36.            Log.e(TAG, "Invalid key specification.");  
  37.            throw new IllegalArgumentException(e);  
  38.        } catch (Base64DecoderException e) {  
  39.            Log.e(TAG, "Base64 decoding failed.");  
  40.            throw new IllegalArgumentException(e);  
  41.        }  
  42.    }  
  43.   
  44.    /**  
  45.     * Verifies that the signature from the server matches the computed  
  46.     * signature on the data.  Returns true if the data is correctly signed.  
  47.     *  
  48.     * @param publicKey public key associated with the developer account  
  49.     * @param signedData signed data from server  
  50.     * @param signature server signature  
  51.     * @return true if the data and signature match  
  52.     */  
  53.    public static boolean verify(PublicKey publicKey, String signedData, String signature) {  
  54.        Signature sig;  
  55.        try {  
  56.            sig = Signature.getInstance(SIGNATURE_ALGORITHM);  
  57.            sig.initVerify(publicKey);  
  58.            sig.update(signedData.getBytes());  
  59.            if (!sig.verify(Base64.decode(signature))) {  
  60.                Log.e(TAG, "Signature verification failed.");  
  61.                return false;  
  62.            }  
  63.            return true;  
  64.        } catch (NoSuchAlgorithmException e) {  
  65.            Log.e(TAG, "NoSuchAlgorithmException.");  
  66.        } catch (InvalidKeyException e) {  
  67.            Log.e(TAG, "Invalid key specification.");  
  68.        } catch (SignatureException e) {  
  69.            Log.e(TAG, "Signature exception.");  
  70.        } catch (Base64DecoderException e) {  
  71.            Log.e(TAG, "Base64 decoding failed.");  
  72.        }  
  73.        return false;  
  74.    }  

PublicKey:

這個PublicKey是用來驗證支付結果的,所以這絕對是個Key,不可以讓其他人知道的,這個Key放到支付服務器端,本地不存。如果是拷貝Sample裏的代碼使用,會發現在 new IabHelper 的時候會在構造方法裏傳遞這個 

[html] view plain copy
  1. base64EncodedPublicKey  

其實Sample只是用這個  PublicKey 做本地驗證的,和初始化無關,你仔細看下 IabHelper 源碼就知道了,所以本地不存這個 PublicKey,當  new IabHelper 的時候,可以隨便傳個字符串,也可把 IabHelper 構造方法改一下,不再傳這個值。

samples裏的這段代碼寫的很有意思,能看出笑點不?

單機遊戲的話,想辦法把這個key存到某個地方加個密什麼的,最好不要直接寫到代碼裏。(其實對於單機遊戲,如果沒有自己的服務器來驗證支付結果,本地不管如何操作,都是很容易被破解的,如果遊戲比較大賣,推薦自己寫個支付服務器端來驗證支付結果)。

[html] view plain copy
  1. /* base64EncodedPublicKey should be YOUR APPLICATION'S PUBLIC KEY  
  2.  * (that you got from the Google Play developer console). This is not your  
  3.  * developer public key, it's the *app-specific* public key.  
  4.  *  
  5.  * Instead of just storing the entire literal string here embedded in the  
  6.  * program,  construct the key at runtime from pieces or  
  7.  * use bit manipulation (for example, XOR with some other string) to hide  
  8.  * the actual key.  The key itself is not secret information, but we don't  
  9.  * want to make it easy for an attacker to replace the public key with one  
  10.  * of their own and then fake messages from the server.  
  11.  */  
  12. String base64EncodedPublicKey = "CONSTRUCT_YOUR_KEY_AND_PLACE_IT_HERE";  
  13.   
  14. // Some sanity checks to see if the developer (that's you!) really followed the  
  15. // instructions to run this sample (don't put these checks on your app!)  
  16. if (base64EncodedPublicKey.contains("CONSTRUCT_YOUR")) {  
  17.     throw new RuntimeException("Please put your app's public key in MainActivity.java. See README.");  
  18. }  
  19. if (getPackageName().startsWith("com.example")) {  
  20.     throw new RuntimeException("Please change the sample's package name! See README.");  
  21. }  

本地服務器驗證補充:

關於支付結果的驗證,本地服務器除了用publicKey做簽名驗證外,還可以到Google後臺請求下支付結果驗證。這個需要本地服務器和Google服務器交互通信。可以參考這個文檔。


參考地址:

https://developer.android.google.cn/google/play/developer-api.html#publishing_api_overview


不過對於國內的開發者而言,在Google日益被封鎖加重的情況下,在與Google服務器通信上絕對會有障礙,因爲通信阻礙,會導致你驗證失敗,所以這個功能可選,有興趣的可以添加上。

補充1:

如果是直接用samples的代碼的話還需要注意幾點。第一,把錯誤提示改成用戶友好型的。因爲samples的錯誤提示主要是給開發者看的,所以提示的很詳細,但是用戶不需要,你只要告訴用戶成功,失敗以及簡單的失敗原因就行了。第二,在發佈正式版時把打印信息關掉。第三,修改類名和變量名。

補充2:

如果在測試支付時遇到一下錯誤,可做的處理。

1.當前應用程序不支持購買此商品:確定你手機上裝的程序包名和簽名和後臺上傳的一致。p.s.上傳後臺後APK需要等一段時間才能生效。

2.購買的商品不存在 :確保你代碼裏的商品名字和後臺的一致,如果一致,則可能需要等一兩個小時再測試,Google後臺的問題。

3.loading了很長時間,最後給你提示未知錯誤:這個不管它,Google後臺的問題,等會再測。

最後國內開發者確保是在vpn下進行測試!!!!

寫在後面:

以上就是Google In-app Billing的代碼添加了,其實就是把samples講了一下吐舌頭,所以還是推薦去看下官方文檔和samples吧,在那裏你會學到更多。


追加更新20150129:

1. 最近有童鞋在問,測試支付時遇到賬號未認證的錯誤,怎麼解決。我當時測試的時候沒遇見這個錯誤,個人估計可能是你的應用還沒有通過谷歌的審覈,上傳後臺要等半個小時或者一個小時。當然如果不是這個原因的話,請解決了此問題的童鞋私信我,我把它加到博客裏,這樣可以幫助其他人,先謝過啦。
2.最近在搞unity,所以很少上 CSDN了(也很久沒更新博客了,慚愧啊慚愧)。留個郵箱吧,有問題的可以發郵件給我 [email protected] 。努力學習,共同進步!


追加更新20150817:

關於 Authentication is required. You need to sign into your Google Account. 的解決。

1.先前有童鞋問過這個問題,就是支付測試時提示賬號未認證,需要登錄Google賬號。目前已知的解決辦法就是在手機上使用測試賬號(在上篇介紹過如何設置測試賬號),不要用普通的Google賬號。

P.S.添加使用的測試賬號,只要是Gmail賬號即可,但是不要用自己的開發者賬號,就是說不要在測試機上登陸開發者賬號進行測試,切記,切記。否則就會出現

"無法購買您要買的商品" 的錯誤!

2.在 android 5.0 上測試時遇見 

 java.lang.IllegalArgumentException: Service Intent must be explicit: Intent { act=com.android.vending.billing.InAppBillingService.BIND } 
根據童鞋反應,情況是這樣滴。。。在5.0之前通過 Intent 調用 bindService()這個方法時使用 explicit intent 是推薦級別,但是在5.0之後是強制使用了。
解決辦法:
1.升級In-app Billing。
使用最新的Billing libraries。我看了下Billing Version已經升到5了。大概看了下,沒有新增公共方法。
 
 
2.targetSdkVersion降級
如果targetSdkVersion用的是 5.0 級別的 API 21,那就把targetSdkVersion降到 19(對應的版本是 4.4.2)。同時記得修改工程屬性文件 project.properties 中的 target .

追加更新20151030:

最近很多童鞋來問,APK上傳後臺了,設置爲Alpha或者Beta版了,商品也設置好了,也等了一個小時,有的等了一天了。。。等等,都準備好了,但是在測試購買商品的時候還是不能購買QAQ~  後來才發現,他們上傳到Google後臺的商品雖然設置爲Alpha或者Beta版了,但是還是處於草稿(Draft)狀態,而不是發佈狀態,所以無法測試。出現這個的原因主要是Google後臺的 APP信息沒有填寫完整。在把自己的程序上傳到後臺後,需要填寫相關的信息,雖然是測試,但是不要以爲就寫個App名字就完事了,你需要填寫完整的App信息纔可以發佈,即使不是發佈正式版。還有商品也要設置爲發佈狀態,印象中商品也會有草稿狀態。

所以把App上傳到Google後臺,等待一段時間後,要記得檢查你App的狀態,看是不是草稿狀態,後臺的右上角也有提示“爲何此App無法發佈?”,點進去看看也會有收穫。
切記要細心~
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章