1. 程式人生 > >Google play billing(Google play 內支付) 下篇

Google play billing(Google play 內支付) 下篇

開篇:
如billing開發文件所說,要在你的應用中實現In-app Billing只需要完成以下幾步就可以了。


第一,把你上篇下載的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!匯入成功後,呼叫其中的一個方法就可以了。

	/**
	 * Check the device to make sure it has the Google Play Services APK.If
	 * it doesn't, display a dialog that allows users to download the APK from
	 * the Google Play Store or enable it in the device's system settings
	 */
	private boolean checkPlayServices()
	{
		int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
		if(resultCode != ConnectionResult.SUCCESS)
		{
			if(GooglePlayServicesUtil.isUserRecoverableError(resultCode))
			{
				GooglePlayServicesUtil.getErrorDialog(resultCode, this,
						PLAY_SERVICES_RESOLUTION_REQUEST).show();
			}
			else
			{
				Log.i(TAG, "This device is not supported");
				finish();
			}
			return false;
		}
		return true;
	}

如果當前裝置的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",可以在程式啟動的時候遍歷下裝置上的包名,如果沒有這兩個東西就引導使用者去安裝。

遍歷包名方法

    //Check Google Play
    protected boolean isHaveGooglePlay(Context context, String packageName)
    {
    	//Get PackageManager
    	final PackageManager packageManager = context.getPackageManager();

    	//Get The All Install App Package Name
    	List<PackageInfo> pInfo = packageManager.getInstalledPackages(0);
    	
    	//Create Name List
    	List<String> pName = new ArrayList<String>();
    	
    	//Add Package Name into Name List
    	if(pInfo != null){
    		for(int i=0; i<pInfo.size(); i++){
    			String pn = pInfo.get(i).packageName;
    			pName.add(pn);
    			
    			//Log.v("Package Name", "PackAgeName: = " + pn);
    		}
    	}
    	
    	//Check 
    	return pName.contains(packageName);
    }
提示安裝方法
				 Uri uri = Uri.parse("market://details?id=" + "要安裝程式的包名");
	                Intent it = new Intent(Intent.ACTION_VIEW, uri); 
	                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,來呼叫這個方法,根據不同的回撥裡做相應的處理。

/**
     * Starts the setup process. This will start up the setup process asynchronously.
     * You will be notified through the listener when the setup process is complete.
     * This method is safe to call from a UI thread.
     *
     * @param listener The listener to notify when the setup process is complete.
     */
    public void startSetup(final OnIabSetupFinishedListener listener) {
        // If already set up, can't do it again.
        checkNotDisposed();
        if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");

        // Connection to IAB service
        logDebug("Starting in-app billing setup.");
        mServiceConn = new ServiceConnection() {
            @Override
            public void onServiceDisconnected(ComponentName name) {
                logDebug("Billing service disconnected.");
                mService = null;
            }

            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                if (mDisposed) return;
                logDebug("Billing service connected.");
                mService = IInAppBillingService.Stub.asInterface(service);
                String packageName = mContext.getPackageName();
                try {
                    logDebug("Checking for in-app billing 3 support.");

                    // check for in-app billing v3 support
                    int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
                    if (response != BILLING_RESPONSE_RESULT_OK) {
                        if (listener != null) listener.onIabSetupFinished(new IabResult(response,
                                "Error checking for billing v3 support."));

                        // if in-app purchases aren't supported, neither are subscriptions.
                        mSubscriptionsSupported = false;
                        return;
                    }
                    logDebug("In-app billing version 3 supported for " + packageName);

                    // check for v3 subscriptions support
                    response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
                    if (response == BILLING_RESPONSE_RESULT_OK) {
                        logDebug("Subscriptions AVAILABLE.");
                        mSubscriptionsSupported = true;
                    }
                    else {
                        logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
                    }

                    mSetupDone = true;
                }
                catch (RemoteException e) {
                    if (listener != null) {
                        listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
                                                    "RemoteException while setting up in-app billing."));
                    }
                    e.printStackTrace();
                    return;
                }

                if (listener != null) {
                    listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
                }
            }
        };

        Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
        serviceIntent.setPackage("com.android.vending");
        if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
            // service available to handle that Intent
            mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
        }
        else {
            // no service available to handle that Intent
            if (listener != null) {
                listener.onIabSetupFinished(
                        new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
                        "Billing service unavailable on device."));
            }
        }
    }
samples中的程式碼
// Create the helper, passing it our context and the public key to verify signatures with
        Log.d(TAG, "Creating IAB helper.");
        mHelper = new IabHelper(this, base64EncodedPublicKey);


        // enable debug logging (for a production application, you should set this to false).
        mHelper.enableDebugLogging(true);


        // Start setup. This is asynchronous and the specified listener
        // will be called once setup completes.
        Log.d(TAG, "Starting setup.");
        mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
            public void onIabSetupFinished(IabResult result) {
                Log.d(TAG, "Setup finished.");


                if (!result.isSuccess()) {
                    // Oh noes, there was a problem.
                    complain("Problem setting up in-app billing: " + result);
                    return;
                }


                // Have we been disposed of in the meantime? If so, quit.
                if (mHelper == null) return;


                // IAB is fully set up. Now, let's get an inventory of stuff we own.
                Log.d(TAG, "Setup successful. Querying inventory.");
                mHelper.queryInventoryAsync(mGotInventoryListener);
            }
        });
    }

02.查詢商品

在setup方法的最後有一個

 mHelper.queryInventoryAsync(mGotInventoryListener);
是用來查詢你目前擁有的商品的。其中的回撥的程式碼如下
// Listener that's called when we finish querying the items and subscriptions we own
    IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
        public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
            Log.d(TAG, "Query inventory finished.");

            // Have we been disposed of in the meantime? If so, quit.
            if (mHelper == null) return;

            // Is it a failure?
            if (result.isFailure()) {
                complain("Failed to query inventory: " + result);
                return;
            }

            Log.d(TAG, "Query inventory was successful.");

            /*
             * Check for items we own. Notice that for each purchase, we check
             * the developer payload to see if it's correct! See
             * verifyDeveloperPayload().
             */

            // Do we have the premium upgrade?
            Purchase premiumPurchase = inventory.getPurchase(SKU_PREMIUM);
            mIsPremium = (premiumPurchase != null && verifyDeveloperPayload(premiumPurchase));
            Log.d(TAG, "User is " + (mIsPremium ? "PREMIUM" : "NOT PREMIUM"));

            // Do we have the infinite gas plan?
            Purchase infiniteGasPurchase = inventory.getPurchase(SKU_INFINITE_GAS);
            mSubscribedToInfiniteGas = (infiniteGasPurchase != null &&
                    verifyDeveloperPayload(infiniteGasPurchase));
            Log.d(TAG, "User " + (mSubscribedToInfiniteGas ? "HAS" : "DOES NOT HAVE")
                        + " infinite gas subscription.");
            if (mSubscribedToInfiniteGas) mTank = TANK_MAX;

            // Check for gas delivery -- if we own gas, we should fill up the tank immediately
            Purchase gasPurchase = inventory.getPurchase(SKU_GAS);
            if (gasPurchase != null && verifyDeveloperPayload(gasPurchase)) {
                Log.d(TAG, "We have gas. Consuming it.");
                mHelper.consumeAsync(inventory.getPurchase(SKU_GAS), mConsumeFinishedListener);
                return;
            }

            updateUi();
            setWaitScreen(false);
            Log.d(TAG, "Initial inventory query finished; enabling main UI.");
        }
    };

因為目前我們的內購商品是可重複購買的,所以在成功查詢到我們已經購買的商品後進行了消耗商品操作。消耗的程式碼在這裡
            // Check for gas delivery -- if we own gas, we should fill up the tank immediately
            Purchase gasPurchase = inventory.getPurchase(SKU_GAS);
            if (gasPurchase != null && verifyDeveloperPayload(gasPurchase)) {
                Log.d(TAG, "We have gas. Consuming it.");
                mHelper.consumeAsync(inventory.getPurchase(SKU_GAS), mConsumeFinishedListener);
                return;
            }

在講消耗前,先解釋下以上這麼操作的原因。在內購商品那裡講過,如果是設定的是可重複商品,當你在成功購買這個商品後是需要主動消耗的,只有消耗成功後才可以再次購買。可能有些人覺得這種設定不好,我的商品本來就是可重複購買的,為什麼還要在買成功後通知Google Play消耗掉商品呢(可能本身商品沒用消耗掉,這只是一種叫法)?我個人覺得這樣設定,第一,可以避免使用者重複下單購買,第二,可以保證每筆消費訂單的唯一。有了以上兩點就可以很好地處理漏單。 so,上面程式碼在成功設定billing後,第一個操作就是查詢擁有的商品,就是做的漏單處理。因為支付過程其實就是你的應用程式----->Google Play程式(通過Google Play Service)------>Google伺服器------->Google Play程式(通過Google Play Service)------>你的應用程式。這樣一個互動過程,還需要網路支援,所以每次支付操作不會保證百分百成功,這樣就會產生漏單現象,就是使用者付費成功了,但是Google Play在通知你的應用程式支付結果時,因為某些原因斷掉了,這樣你的程式就不知道支付是否操作成功了,so,只好在下次進遊戲時查查有沒有已經購買的,但是還沒有消耗的商品,有的話就消耗掉,然後再把商品傳送給使用者。因為這個商品在消耗之前,使用者是無法再次購買的,所以單個使用者就只會對應單一的漏單,不會有重複的漏單。這些資訊都是存到Google伺服器上的,直接調程式碼裡的查詢程式碼就可以了。

02.消耗商品

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

 // Called when consumption is complete
    IabHelper.OnConsumeFinishedListener mConsumeFinishedListener = new IabHelper.OnConsumeFinishedListener() {
        public void onConsumeFinished(Purchase purchase, IabResult result) {
            Log.d(TAG, "Consumption finished. Purchase: " + purchase + ", result: " + result);

            // if we were disposed of in the meantime, quit.
            if (mHelper == null) return;

            // We know this is the "gas" sku because it's the only one we consume,
            // so we don't check which sku was consumed. If you have more than one
            // sku, you probably should check...
            if (result.isSuccess()) {
                // successfully consumed, so we apply the effects of the item in our
                // game world's logic, which in our case means filling the gas tank a bit
                Log.d(TAG, "Consumption successful. Provisioning.");
                mTank = mTank == TANK_MAX ? TANK_MAX : mTank + 1;
                saveData();
                alert("You filled 1/4 tank. Your tank is now " + String.valueOf(mTank) + "/4 full!");
            }
            else {
                complain("Error while consuming: " + result);
            }
            updateUi();
            setWaitScreen(false);
            Log.d(TAG, "End consumption flow.");
        }
    };

程式碼比較簡單,針對自己的遊戲邏輯,在裡面稍做改動即可。

03.購買商品

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

 /**
     * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
     * which will involve bringing up the Google Play screen. The calling activity will be paused while
     * the user interacts with Google Play, and the result will be delivered via the activity's
     * {@link android.app.Activity#onActivityResult} method, at which point you must call
     * this object's {@link #handleActivityResult} method to continue the purchase flow. This method
     * MUST be called from the UI thread of the Activity.
     *
     * @param act The calling activity.
     * @param sku The sku of the item to purchase.
     * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)
     * @param requestCode A request code (to differentiate from other responses --
     *     as in {@link android.app.Activity#startActivityForResult}).
     * @param listener The listener to notify when the purchase process finishes
     * @param extraData Extra data (developer payload), which will be returned with the purchase data
     *     when the purchase completes. This extra data will be permanently bound to that purchase
     *     and will always be returned when the purchase is queried.
     */
    public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,
                        OnIabPurchaseFinishedListener listener, String extraData) {
        checkNotDisposed();
        checkSetupDone("launchPurchaseFlow");
        flagStartAsync("launchPurchaseFlow");
        IabResult result;

        if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
            IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
                    "Subscriptions are not available.");
            flagEndAsync();
            if (listener != null) listener.onIabPurchaseFinished(r, null);
            return;
        }

        try {
            logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
            Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
            int response = getResponseCodeFromBundle(buyIntentBundle);
            if (response != BILLING_RESPONSE_RESULT_OK) {
                logError("Unable to buy item, Error response: " + getResponseDesc(response));
                flagEndAsync();
                result = new IabResult(response, "Unable to buy item");
                if (listener != null) listener.onIabPurchaseFinished(result, null);
                return;
            }

            PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
            logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
            mRequestCode = requestCode;
            mPurchaseListener = listener;
            mPurchasingItemType = itemType;
            act.startIntentSenderForResult(pendingIntent.getIntentSender(),
                                           requestCode, new Intent(),
                                           Integer.valueOf(0), Integer.valueOf(0),
                                           Integer.valueOf(0));
        }
        catch (SendIntentException e) {
            logError("SendIntentException while launching purchase flow for sku " + sku);
            e.printStackTrace();
            flagEndAsync();

            result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
            if (listener != null) listener.onIabPurchaseFinished(result, null);
        }
        catch (RemoteException e) {
            logError("RemoteException while launching purchase flow for sku " + sku);
            e.printStackTrace();
            flagEndAsync();

            result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
            if (listener != null) listener.onIabPurchaseFinished(result, null);
        }
    }

以上是IabHelper中的支付購買程式碼,其中包括了重複購買商品型別和一次購買商品型別的處理。主要的程式碼是try裡面的這一塊
        try {
            logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
            Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
            int response = getResponseCodeFromBundle(buyIntentBundle);
            if (response != BILLING_RESPONSE_RESULT_OK) {
                logError("Unable to buy item, Error response: " + getResponseDesc(response));
                flagEndAsync();
                result = new IabResult(response, "Unable to buy item");
                if (listener != null) listener.onIabPurchaseFinished(result, null);
                return;
            }

            PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
            logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
            mRequestCode = requestCode;
            mPurchaseListener = listener;
            mPurchasingItemType = itemType;
            act.startIntentSenderForResult(pendingIntent.getIntentSender(),
                                           requestCode, new Intent(),
                                           Integer.valueOf(0), Integer.valueOf(0),
                                           Integer.valueOf(0));
        }
一,呼叫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.如上面程式碼演示。

三,進行支付

act.startIntentSenderForResult(pendingIntent.getIntentSender(),
                               requestCode, new Intent(),
                               Integer.valueOf(0), Integer.valueOf(0),
                               Integer.valueOf(0));
這個方法是Activity中的一個方法,呼叫這個方法後,回有一個回撥來接收結果。除了第一個PengdingIntent引數外,其他的可以按引數型別隨便寫。

四,支付完成

 /**
     * Handles an activity result that's part of the purchase flow in in-app billing. If you
     * are calling {@link #launchPurchaseFlow}, then you must call this method from your
     * Activity's {@link [email protected]} method. This method
     * MUST be called from the UI thread of the Activity.
     *
     * @param requestCode The requestCode as you received it.
     * @param resultCode The resultCode as you received it.
     * @param data The data (Intent) as you received it.
     * @return Returns true if the result was related to a purchase flow and was handled;
     *     false if the result was not related to a purchase, in which case you should
     *     handle it normally.
     */
    public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
        IabResult result;
        if (requestCode != mRequestCode) return false;

        checkNotDisposed();
        checkSetupDone("handleActivityResult");

        // end of async purchase operation that started on launchPurchaseFlow
        flagEndAsync();

        if (data == null) {
            logError("Null data in IAB activity result.");
            result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
            if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
            return true;
        }

        int responseCode = getResponseCodeFromIntent(data);
        String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
        String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);

        if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
            logDebug("Successful resultcode from purchase activity.");
            logDebug("Purchase data: " + purchaseData);
            logDebug("Data signature: " + dataSignature);
            logDebug("Extras: " + data.getExtras());
            logDebug("Expected item type: " + mPurchasingItemType);

            if (purchaseData == null || dataSignature == null) {
                logError("BUG: either purchaseData or dataSignature is null.");
                logDebug("Extras: " + data.getExtras().toString());
                result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
                if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
                return true;
            }

            Purchase purchase = null;
            try {
                purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
                String sku = purchase.getSku();

                // Verify signature
                if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
                    logError("Purchase signature verification FAILED for sku " + sku);
                    result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
                    if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase);
                    return true;
                }
                logDebug("Purchase signature successfully verified.");
            }
            catch (JSONException e) {
                logError("Failed to parse purchase data.");
                e.printStackTrace();
                result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
                if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
                return true;
            }

            if (mPurchaseListener != null) {
                mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
            }
        }
        else if (resultCode == Activity.RESULT_OK) {
            // result code was OK, but in-app billing response was not OK.
            logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
            if (mPurchaseListener != null) {
                result = new IabResult(responseCode, "Problem purchashing item.");
                mPurchaseListener.onIabPurchaseFinished(result, null);
            }
        }
        else if (resultCode == Activity.RESULT_CANCELED) {
            logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
            result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
            if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
        }
        else {
            logError("Purchase failed. Result code: " + Integer.toString(resultCode)
                    + ". Response: " + getResponseDesc(responseCode));
            result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
            if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
        }
        return true;
    }

    public Inventory queryInventory(boolean querySkuDetails, List<String> moreSkus) throws IabException {
        return queryInventory(querySkuDetails, moreSkus, null);
    }

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

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        Log.d(TAG, "onActivityResult(" + requestCode + "," + resultCode + "," + data);
        if (mHelper == null) return;

        // Pass on the activity result to the helper for handling
        if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {
            // not handled, so handle it ourselves (here's where you'd
            // perform any handling of activity results not related to in-app
            // billing...
            super.onActivityResult(requestCode, resultCode, data);
        }
        else {
            Log.d(TAG, "onActivityResult handled by IABUtil.");
        }
    }
這個方法會在支付結束,你的程式重新回到前臺的時候呼叫。在這個方法中可以看到
!mHelper.handleActivityResult(requestCode, resultCode, data)
這裡呼叫了 IabHelper 裡的 handleActivityResult 方法。然後再到此方法裡會看到呼叫 PurchseListener 的地方
if (mPurchaseListener != null) {
                mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
            }

至此就可以知道 OnIabPurchaseFinishedListener 是從哪呼叫的了。然後在 handleActivityResult  方法裡還可以看到這段程式碼
 // Verify signature
                if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
                    logError("Purchase signature verification FAILED for sku " + sku);
                    result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
                    if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase);
                    return true;
                }
                logDebug("Purchase signature successfully verified.");

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

Purchase signature verification FAILED for sku xxx

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

因為在 Security.java 中 會驗證失敗
   public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
        if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
                TextUtils.isEmpty(signature)) {
            Log.e(TAG, "Purchase verification failed: missing data.");
            return false;
        }

        PublicKey key = Security.generatePublicKey(base64PublicKey);
        return Security.verify(key, signedData, signature);
    }

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

解決方法:

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

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

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

Failed to query inventory: IabResult: Error refreshing inventory (querying owned items). (response: -1003:Purchase signature verification failed)
之所以出現這個錯誤,是因為在查詢的時候,也會執行 Security 中的 verifyPurchase 方法。追本溯源,一步一步的查詢程式碼,會在 IabHelper 中的
int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {... ...}

此方法中找到Security 呼叫 verifyPurchase 。


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

 注意

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

Purchase purchase = new Purchase(itemType, purchaseData, signature);
                
                inv.addPurchase(purchase);

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

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


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

        int responseCode = getResponseCodeFromIntent(data);
        String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
        String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
就是purchaseData和dataSignature。驗證支付就是需要這兩個引數和publicKey,例子裡的驗證方法是寫在Security.java裡的。裡面寫了三個方法來完成支付結果的驗證。

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

 /**
     * Verifies that the data was signed with the given signature, and returns
     * the verified purchase. The data is in JSON format and signed
     * with a private key. The data also contains the {@link PurchaseState}
     * and product ID of the purchase.
     * @param base64PublicKey the base64-encoded public key to use for verifying.
     * @param signedData the signed JSON string (signed, not encrypted)
     * @param signature the signature for the data, signed with the private key
     */
    public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
        if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
                TextUtils.isEmpty(signature)) {
            Log.e(TAG, "Purchase verification failed: missing data.");
            return false;
        }

        PublicKey key = Security.generatePublicKey(base64PublicKey);
        return Security.verify(key, signedData, signature);
    }

    /**
     * Generates a PublicKey instance from a string containing the
     * Base64-encoded public key.
     *
     * @param encodedPublicKey Base64-encoded public key
     * @throws IllegalArgumentException if encodedPublicKey is invalid
     */
    public static PublicKey generatePublicKey(String encodedPublicKey) {
        try {
            byte[] decodedKey = Base64.decode(encodedPublicKey);
            KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
            return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } catch (InvalidKeySpecException e) {
            Log.e(TAG, "Invalid key specification.");
            throw new IllegalArgumentException(e);
        } catch (Base64DecoderException e) {
            Log.e(TAG, "Base64 decoding failed.");
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * Verifies that the signature from the server matches the computed
     * signature on the data.  Returns true if the data is correctly signed.
     *
     * @param publicKey public key associated with the developer account
     * @param signedData signed data from server
     * @param signature server signature
     * @return true if the data and signature match
     */
    public static boolean verify(PublicKey publicKey, String signedData, String signature) {
        Signature sig;
        try {
            sig = Signature.getInstance(SIGNATURE_ALGORITHM);
            sig.initVerify(publicKey);
            sig.update(signedData.getBytes());
            if (!sig.verify(Base64.decode(signature))) {
                Log.e(TAG, "Signature verification failed.");
                return false;
            }
            return true;
        } catch (NoSuchAlgorithmException e) {
            Log.e(TAG, "NoSuchAlgorithmException.");
        } catch (InvalidKeyException e) {
            Log.e(TAG, "Invalid key specification.");
        } catch (SignatureException e) {
            Log.e(TAG, "Signature exception.");
        } catch (Base64DecoderException e) {
            Log.e(TAG, "Base64 decoding failed.");
        }
        return false;
    }

PublicKey:

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

base64EncodedPublicKey

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

samples裡的這段程式碼寫的很有意思,能看出笑點不?

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

        /* base64EncodedPublicKey should be YOUR APPLICATION'S PUBLIC KEY
         * (that you got from the Google Play developer console). This is not your
         * developer public key, it's the *app-specific* public key.
         *
         * Instead of just storing the entire literal string here embedded in the
         * program,  construct the key at runtime from pieces or
         * use bit manipulation (for example, XOR with some other string) to hide
         * the actual key.  The key itself is not secret information, but we don't
         * want to make it easy for an attacker to replace the public key with one
         * of their own and then fake messages from the server.
         */
        String base64EncodedPublicKey = "CONSTRUCT_YOUR_KEY_AND_PLACE_IT_HERE";

        // Some sanity checks to see if the developer (that's you!) really followed the
        // instructions to run this sample (don't put these checks on your app!)
        if (base64EncodedPublicKey.contains("CONSTRUCT_YOUR")) {
            throw new RuntimeException("Please put your app's public key in MainActivity.java. See README.");
        }
        if (getPackageName().startsWith("com.example")) {
            throw new RuntimeException("Please change the sample's package name! See README.");
        }

本地伺服器驗證補充:

關於支付結果的驗證,本地伺服器除了用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無法釋出?”,點進去看看也會有收穫。 切記要細心~

相關推薦

Google play billing(Google play 支付) 下篇

開篇:如billing開發文件所說,要在你的應用中實現In-app Billing只需要完成以下幾步就可以了。 第一,把你上篇下載的AIDL檔案新增到你的工程裡,第二,把<uses-permission android:name="com.android.v

Google play billing(Google play 支付)

http://www.bubuko.com/infodetail-930440.html 如billing開發文件所說,要在你的應用中實現In-app Billing只需要完成以下幾步就可以了。 第一,把你上篇下載的AIDL檔案新增到你的工程裡,第二,把 <us

in-app-billing for google playgoogle應用付費 v3)

詳細文件參考: https://developer.android.com/google/play/billing/billing_integrate.html#billing-add-aidl         Google的In-app Billing, V3版本的介面是

Google play之應用支付接入

前言 官方文件 官方Sample google提供的官方sample是已經對官方api經過封裝了的,而google官方文件是按照最原裝的程式碼進行 描述的,所以本文將按照sample方式接入 SDK接入 google應用內支付不用新

Google play billing建付費測試問題總結

這幾天接入google Play支付,也算是碰壁不少,到處爬帖子,總算是調通了,下面把遇到的問題分享下,方便後來人少繞彎路吧。 至於google Play如果整合到安卓工程和如何寫付費程式碼我這裡就不介紹了,網上很多介紹的帖子介紹的很不錯,自行百度和google吧。 主要說

Google登入接入(play-games-plugin-for-unity)

Google後臺配置 進入google後臺:https://play.google.com/apps/publish/?hl=zh&account=5458629459868388661#GameListPlace 新版外掛下載:https://github.com/playgame

Google Android應用支付訂單服務端驗證

       最近公司的APP新增了收費版本,針對一些高階功能需要使用者付費才能使用,付費的方式是使用者通過應用內支付去訂閱一個月或一年的賬戶高階許可權,相當於QQ裡面的VIP功能。        大概的流程是使用者下載APP後註冊之後預設為普通使用者,使用者通過應用內支付

Android Eclipse實現Google Pay支付

現在越來越多的人都開始接觸了海外的支付方式,而google的官方支付作為一大支付方式,並且對接過程中遇到的坑較多,而我們又有可能用到,所以今天在這裡寫一下之前對接google的一些自我理解。希望可以幫助到大家: 先貼一份我對接Google支付的流程圖:(根據實

Google Gson 禁止序列化部類

本文內容大多基於官方文件和網上前輩經驗總結,經過個人實踐加以整理積累,僅供參考。 1 新建包含內部類的 POJO 類 public class User { private String account; private Stri

Google面試題集錦(附答案/解析)

將下列表達式按照複雜度排序 2^n n^Googol (其中 Googol = 10^100) n! n^n 按照複雜度從低到高為 n^Googol 2^n n! n^n 1024! 末尾有多少個0? 答案:末尾0的個數取決於乘法中因子2和5的個數。顯然乘法中因子2

Linux下安裝Google SDK 配置Google API翻譯環境

mail 代碼 到你 .tar.gz kpi reat export $path zone 1、準備工作 1.1 查看系統Python版本 Linux 安裝Google SDK時要求安裝Python 2.7或以上版本 可以用 python -V 查看當前環境下的 Pyt

iOS應用支付(IAP)服務端端校驗詳解

imageview sof 客戶端 標識 知識庫 ndb json replace undle IAP流程 IAP流程分為兩種: 一種是直接使用Apple的服務器進行購買和驗證, 另一種就是自己假設服務器進行驗證。由於國內網絡連接Apple服務器驗證非常慢,而且也為了防止黑

怎樣用Google APIs和Google的應用系統進行集成(2)----Google APIs的全部的RESTFul服務一覽

account view coo bean tps pla ads count dsm 上篇文章,我提到了,Google APIs暴露了86種不同種類和版本號的API。我們能夠通過在瀏覽器裏面輸入https://www.googleapis.com/discover

play 部署問題 play.exceptions.TemplateExecutionException: No signature of method: java.lang.String.f

前提: 使用 play 區域性更新包生成器V3.0 (http://download.csdn.net/detail/fasttime/9453996)  打包 html,部署至線上後可能出現以下問題。  formatMobile() 是 play.templates.

IOS應用支付IAP從零開始詳解,讓你少踩坑!

前言 什麼是IAP,即in-app-purchase 這幾天一直在搞ios的應用內購,查了很多部落格,發現幾乎沒有一篇部落格可以完整的概括出所有的點,為了防止大夥多次查閱資料,所以寫了這一篇部落格,希望大家能夠跟著我,從零開始,寫一個包含內購的應用出來 流程 一般有以下幾種

修改屬性 通過修改DNS達到不FQ也能訪問Google(僅限於Google)

通過修改DNS達到不FQ也能訪問Google(僅限於Google)   一、前言   不知道各位小夥伴們現在用的搜尋引擎是用Google搜尋還是百度搜索呢?但我個人還是比較極力推薦用Google搜尋的,首先用百度搜索後的結果的前幾項大部分是滿屏的廣告,甚至搜尋的結果並不

[軟體資訊]Google Earth 和 Google Earth Pro 升級到 6.1

Google Earth 6.1升級已經放出,做出了多處改進。首先是更好用的My Places功能。現在你可以按照資料夾的字母順序排序了(如上圖),另外提供更簡潔的搜尋,只需要輸入一個地圖的名字或是其所屬功能,即可在My Places裡高亮顯示出來。My Places功能然後是改進的街景檢視。包括更精確的縮放

Is Machine Learning at Google Falling Apart? Google’s System Doesn’t Believe I‘m a Person.

Is Machine Learning at Google Falling Apart? Google’s System Doesn’t Believe I‘m a Person.Thoughts here are my own, and are not related to my employer.My h

iOS應用支付(IAP)詳解

在iOS開發中如果涉及到虛擬物品的購買,就需要使用IAP服務,我們今天來看看如何實現。 在實現程式碼之前我們先做一些準備工作,一步步來看。 1、IAP流程 IAP流程分為兩種,一種是直接使用Apple的伺服器進行購買和驗證,另一種就是自己假設伺服

iOS應用支付(IAP)的注意事項

來源:http://blog.csdn.net/xinruiios/article/details/9289573 IAP的全稱是In-App Purchase,應用內付費。這種業務模式允許使用者免費下載試用,對應用內提供的商品選擇消費,比如購買遊戲道具,購買遊戲等級