http://www.bubuko.com/infodetail-930440.html
- 如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));
}
- <br style="padding: 0px;" />一,調用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 android.app.Activity@onActivityResult} 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);
}
支付結果返回后會調用上面這個方法,對於支付失敗和其中的錯誤,代碼寫的很清楚,可以自行處理。現在來關注支付成功后的結果驗證。在上面方法中會從支付結果的數據中取得兩個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放到支付服務器端,本地不存。
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服務器交互通信。可以參考這個文檔。

不過對於國內的開發者而言,在Google日益被封鎖加重的情況下,在與Google服務器通信上絕對會有障礙,因為通信阻礙,會導致你驗證失敗,所以這個功能可選,有興趣的可以添加上。
補充1:
如果是直接用samples的代碼的話還需要注意幾點。第一,把錯誤提示改成用戶友好型的。因為samples的錯誤提示主要是給開發者看的,所以提示的很詳細,但是用戶不需要,你只要告訴用戶成功,失敗以及簡單的失敗原因就行了。第二,在發布正式版時把打印信息關掉。第三,修改類名和變量名。
補充2:
如果在測試支付時遇到一下錯誤,可做的處理。
1.當前應用程序不支持購買此商品:確定你手機上裝的程序包名和簽名和后台上傳的一致。p.s.上傳后台后APK需要等一段時間才能生效。
2.購買的商品不存在 :確保你代碼里的商品名字和后台的一致,如果一致,則可能需要等一兩個小時再測試,Google后台的問題。
3.loading了很長時間,最后給你提示未知錯誤:這個不管它,Google后台的問題,等會再測。
最后國內開發者確保是在vpn下進行測試!!!!
寫在后面:
以上就是Google In-app Billing的代碼添加了,其實就是把samples講了一下
,所以還是推薦去看下官方文檔和samples吧,在那里你會學到更多。
