Last active
November 4, 2017 17:39
-
-
Save yigit/4543005 to your computer and use it in GitHub Desktop.
Iab helper from Google in app billing example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Copyright (c) 2012 Google Inc. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package com.path.paymentv3.util; | |
import android.app.Activity; | |
import android.app.PendingIntent; | |
import android.content.ComponentName; | |
import android.content.Context; | |
import android.content.Intent; | |
import android.content.IntentSender.SendIntentException; | |
import android.content.ServiceConnection; | |
import android.os.Bundle; | |
import android.os.Handler; | |
import android.os.IBinder; | |
import android.os.RemoteException; | |
import android.text.TextUtils; | |
import android.util.Log; | |
import com.android.vending.billing.IInAppBillingService; | |
import org.json.JSONException; | |
import java.util.ArrayList; | |
import java.util.List; | |
/** | |
* Provides convenience methods for in-app billing. Create one instance of this | |
* class for your application and use it to process in-app billing operations. | |
* It provides synchronous (blocking) and asynchronous (non-blocking) methods for | |
* many common in-app billing operations, as well as automatic signature | |
* verification. | |
* | |
* After instantiating, you must perform setup in order to start using the object. | |
* To perform setup, call the {@link #startSetup} method and provide a listener; | |
* that listener will be notified when setup is complete, after which (and not before) | |
* you may call other methods. | |
* | |
* After setup is complete, you may query whether the user owns a given item or | |
* not by calling {@link #isOwned}, get all items owned with {@link #getOwnedSkus}, | |
* get an item's price with {@link #getPrice}, amongst others (see documentation | |
* for specific methods). | |
* | |
* Please notice that the object will only have knowledge about owned items; it | |
* will not automatically have information (such as price, description) for items | |
* that are not owned by the user, because the server will not automatically | |
* provide those. In order to query information for an item that's not owned | |
* (such as to display the price to the user before a purchase), you should first | |
* bring the item's sku to the object's knowledge by calling {@link #addSku} | |
* and then perform an inventory refresh by calling {@link #refreshInventory()} | |
* or its corresponding asynchronous version {@link #refreshInventoryAsync}. | |
* | |
* If you know the skus of all the items that you can possibly be interested in, | |
* you can call {@link #addSku} for those items before {@link #startSetup}, and | |
* that way all the information about them will be available from the start, | |
* with no need to refresh the inventory later. | |
* | |
* When you are done with this object, don't forget to call {@link #dispose} | |
* to ensure proper cleanup. This object holds a binding to the in-app billing | |
* service, which will leak unless you dispose of it correctly. If you created | |
* the object on an Activity's onCreate method, then the recommended | |
* place to dispose of it is the Activity's onDestroy method. | |
* | |
* A note about threading: When using this object from a background thread, you may | |
* call the blocking versions of methods; when using from a UI thread, call | |
* only the asynchronous versions and handle the results via callbacks. | |
* Also, notice that you can only call one asynchronous operation at a time; | |
* attempting to start a second asynchronous operation while the first one | |
* has not yet completed will result in an exception being thrown. | |
* | |
* @author Bruno Oliveira (Google) | |
* | |
*/ | |
public class IabHelper { | |
// Is debug logging enabled? | |
boolean mDebugLog = false; | |
String mDebugTag = "IabHelper"; | |
// Is setup done? | |
boolean mSetupDone = false; | |
// Is an asynchronous operation in progress? | |
// (only one at a time can be in progress) | |
boolean mAsyncInProgress = false; | |
// (for logging/debugging) | |
// if mAsyncInProgress == true, what asynchronous operation is in progress? | |
String mAsyncOperation = ""; | |
// Context we were passed during initialization | |
Context mContext; | |
// Connection to the service | |
IInAppBillingService mService; | |
ServiceConnection mServiceConn; | |
// The request code used to launch purchase flow | |
int mRequestCode; | |
// Public key for verifying signature, in base64 encoding | |
String mSignatureBase64 = null; | |
// Billing response codes | |
public static final int BILLING_RESPONSE_RESULT_OK = 0; | |
public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1; | |
public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3; | |
public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4; | |
public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5; | |
public static final int BILLING_RESPONSE_RESULT_ERROR = 6; | |
public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7; | |
public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8; | |
// IAB Helper error codes | |
public static final int IABHELPER_ERROR_BASE = -1000; | |
public static final int IABHELPER_REMOTE_EXCEPTION = -1001; | |
public static final int IABHELPER_BAD_RESPONSE = -1002; | |
public static final int IABHELPER_VERIFICATION_FAILED = -1003; | |
public static final int IABHELPER_SEND_INTENT_FAILED = -1004; | |
public static final int IABHELPER_USER_CANCELLED = -1005; | |
public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006; | |
public static final int IABHELPER_MISSING_TOKEN = -1007; | |
public static final int IABHELPER_UNKNOWN_ERROR = -1008; | |
// Keys for the responses from InAppBillingService | |
public static final String RESPONSE_CODE = "RESPONSE_CODE"; | |
public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST"; | |
public static final String RESPONSE_BUY_INTENT = "BUY_INTENT"; | |
public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA"; | |
public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE"; | |
public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST"; | |
public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST"; | |
public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST"; | |
public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN"; | |
// Item type: in-app item | |
public static final String ITEM_TYPE_INAPP = "inapp"; | |
// some fields on the getSkuDetails response bundle | |
public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST"; | |
public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST"; | |
/** | |
* Creates an instance. After creation, it will not yet be ready to use. You must perform | |
* setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not | |
* block and is safe to call from a UI thread. | |
* | |
* @param ctx Your application or Activity context. Needed to bind to the in-app billing service. | |
* @param base64PublicKey Your application's public key, encoded in base64. | |
* This is used for verification of purchase signatures. You can find your app's base64-encoded | |
* public key in your application's page on Google Play Developer Console. Note that this | |
* is NOT your "developer public key". | |
*/ | |
public IabHelper(Context ctx, String base64PublicKey) { | |
mContext = ctx.getApplicationContext(); | |
mSignatureBase64 = base64PublicKey; | |
logDebug("IAB helper created."); | |
} | |
/** | |
* Enables or disable debug logging through LogCat. | |
*/ | |
public void enableDebugLogging(boolean enable, String tag) { | |
mDebugLog = enable; | |
mDebugTag = tag; | |
} | |
public void enableDebugLogging(boolean enable) { | |
mDebugLog = enable; | |
} | |
/** | |
* Callback for setup process. This listener's {@link #onIabSetupFinished} method is called | |
* when the setup process is complete. | |
*/ | |
public interface OnIabSetupFinishedListener { | |
/** | |
* Called to notify that setup is complete. | |
* | |
* @param result The result of the setup process. | |
*/ | |
public void onIabSetupFinished(IabResult result); | |
} | |
/** | |
* 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. | |
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) { | |
logDebug("Billing service connected."); | |
mService = IInAppBillingService.Stub.asInterface(service); | |
String packageName = mContext.getPackageName(); | |
try { | |
logDebug("Checking for in-app billing 3 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.")); | |
return; | |
} | |
logDebug("In-app billing version 3 supported for " + packageName); | |
mSetupDone = true; | |
} | |
catch (RemoteException e) { | |
if (listener != null) { | |
listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION, | |
"RemoteException while setting up in-app billing.")); | |
} | |
e.printStackTrace(); | |
} | |
if (listener != null) { | |
listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful.")); | |
} | |
} | |
}; | |
mContext.bindService(new Intent("com.android.vending.billing.InAppBillingService.BIND"), | |
mServiceConn, Context.BIND_AUTO_CREATE); | |
} | |
/** | |
* Dispose of object, releasing resources. It's very important to call this | |
* method when you are done with this object. It will release any resources | |
* used by it such as service connections. Naturally, once the object is | |
* disposed of, it can't be used again. | |
*/ | |
public void dispose() { | |
logDebug("Disposing."); | |
mSetupDone = false; | |
if (mServiceConn != null) { | |
logDebug("Unbinding from service."); | |
if (mContext != null) mContext.unbindService(mServiceConn); | |
mServiceConn = null; | |
mService = null; | |
mPurchaseListener = null; | |
} | |
} | |
/** | |
* Callback that notifies when a purchase is finished. | |
*/ | |
public interface OnIabPurchaseFinishedListener { | |
/** | |
* Called to notify that an in-app purchase finished. If the purchase was successful, | |
* then the sku parameter specifies which item was purchased. If the purchase failed, | |
* the sku and extraData parameters may or may not be null, depending on how far the purchase | |
* process went. | |
* | |
* @param result The result of the purchase. | |
* @param info The purchase information (null if purchase failed) | |
*/ | |
public void onIabPurchaseFinished(IabResult result, Purchase info); | |
} | |
// The listener registered on launchPurchaseFlow, which we have to call back when | |
// the purchase finishes | |
OnIabPurchaseFinishedListener mPurchaseListener; | |
/** | |
* Same as calling {@link #launchPurchaseFlow(android.app.Activity, String, int, OnIabPurchaseFinishedListener, String)} | |
* with null as extraData. | |
*/ | |
public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) { | |
launchPurchaseFlow(act, sku, requestCode, listener, ""); | |
} | |
/** | |
* 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 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, int requestCode, OnIabPurchaseFinishedListener listener, String extraData) { | |
checkSetupDone("launchPurchaseFlow"); | |
flagStartAsync("launchPurchaseFlow"); | |
IabResult result; | |
try { | |
logDebug("Constructing buy intent for " + sku); | |
Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, ITEM_TYPE_INAPP, extraData); | |
int response = getResponseCodeFromBundle(buyIntentBundle); | |
if (response != BILLING_RESPONSE_RESULT_OK) { | |
logError("Unable to buy item, Error response: " + getResponseDesc(response)); | |
result = new IabResult(response, "Unable to buy item"); | |
if (listener != null) listener.onIabPurchaseFinished(result, null); | |
//BUG | |
//here, it should return from the method otherwise it still tries to launch | |
//pending intent which is NULL. | |
//it should also call flagEndAsync(); since it won't really make an async request | |
} | |
PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); | |
logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode); | |
mRequestCode = requestCode; | |
mPurchaseListener = listener; | |
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(); | |
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(); | |
result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow"); | |
if (listener != null) listener.onIabPurchaseFinished(result, null); | |
} | |
} | |
/** | |
* 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; | |
checkSetupDone("handleActivityResult"); | |
// end of async purchase operation | |
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()); | |
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(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; | |
} | |
/** | |
* Queries the inventory. This will query all owned items from the server, as well as | |
* information on additional skus, if specified. This method may block or take long to execute. | |
* Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}. | |
* | |
* @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well | |
* as purchase information. | |
* @param moreSkus additional skus to query information on, regardless of ownership. Ignored | |
* if null or if querySkuDetails is false. | |
* @throws IabException if a problem occurs while refreshing the inventory. | |
*/ | |
public Inventory queryInventory(boolean querySkuDetails, List<String> moreSkus) throws IabException { | |
checkSetupDone("queryInventory"); | |
try { | |
Inventory inv = new Inventory(); | |
int r = queryPurchases(inv); | |
if (r != BILLING_RESPONSE_RESULT_OK) { | |
throw new IabException(r, "Error refreshing inventory (querying owned items)."); | |
} | |
if (querySkuDetails) { | |
r = querySkuDetails(inv, moreSkus); | |
if (r != BILLING_RESPONSE_RESULT_OK) { | |
throw new IabException(r, "Error refreshing inventory (querying prices of items)."); | |
} | |
} | |
return inv; | |
} | |
catch (RemoteException e) { | |
throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e); | |
} | |
catch (JSONException e) { | |
throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e); | |
} | |
} | |
/** | |
* Listener that notifies when an inventory query operation completes. | |
*/ | |
public interface QueryInventoryFinishedListener { | |
/** | |
* Called to notify that an inventory query operation completed. | |
* | |
* @param result The result of the operation. | |
* @param inv The inventory. | |
*/ | |
public void onQueryInventoryFinished(IabResult result, Inventory inv); | |
} | |
/** | |
* Asynchronous wrapper for inventory query. This will perform an inventory | |
* query as described in {@link #queryInventory}, but will do so asynchronously | |
* and call back the specified listener upon completion. This method is safe to | |
* call from a UI thread. | |
* | |
* @param querySkuDetails as in {@link #queryInventory} | |
* @param moreSkus as in {@link #queryInventory} | |
* @param listener The listener to notify when the refresh operation completes. | |
*/ | |
public void queryInventoryAsync(final boolean querySkuDetails, | |
final List<String> moreSkus, | |
final QueryInventoryFinishedListener listener) { | |
final Handler handler = new Handler(); | |
checkSetupDone("queryInventory"); | |
flagStartAsync("refresh inventory"); | |
(new Thread(new Runnable() { | |
public void run() { | |
IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful."); | |
Inventory inv = null; | |
try { | |
inv = queryInventory(querySkuDetails, moreSkus); | |
} | |
catch (IabException ex) { | |
result = ex.getResult(); | |
} | |
flagEndAsync(); | |
final IabResult result_f = result; | |
final Inventory inv_f = inv; | |
handler.post(new Runnable() { | |
public void run() { | |
listener.onQueryInventoryFinished(result_f, inv_f); | |
} | |
}); | |
} | |
})).start(); | |
} | |
public void queryInventoryAsync(QueryInventoryFinishedListener listener) { | |
queryInventoryAsync(true, null, listener); | |
} | |
public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) { | |
queryInventoryAsync(querySkuDetails, null, listener); | |
} | |
/** | |
* Consumes a given in-app product. Consuming can only be done on an item | |
* that's owned, and as a result of consumption, the user will no longer own it. | |
* This method may block or take long to return. Do not call from the UI thread. | |
* For that, see {@link #consumeAsync}. | |
* | |
* @param itemInfo The PurchaseInfo that represents the item to consume. | |
* @throws IabException if there is a problem during consumption. | |
*/ | |
void consume(Purchase itemInfo) throws IabException { | |
checkSetupDone("consume"); | |
try { | |
String token = itemInfo.getToken(); | |
String sku = itemInfo.getSku(); | |
if (token == null || token.equals("")) { | |
logError("Can't consume "+ sku + ". No token."); | |
throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: " | |
+ sku + " " + itemInfo); | |
} | |
logDebug("Consuming sku: " + sku + ", token: " + token); | |
int response = mService.consumePurchase(3, mContext.getPackageName(), token); | |
if (response == BILLING_RESPONSE_RESULT_OK) { | |
logDebug("Successfully consumed sku: " + sku); | |
} | |
else { | |
logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response)); | |
throw new IabException(response, "Error consuming sku " + sku); | |
} | |
} | |
catch (RemoteException e) { | |
throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e); | |
} | |
} | |
/** | |
* Callback that notifies when a consumption operation finishes. | |
*/ | |
public interface OnConsumeFinishedListener { | |
/** | |
* Called to notify that a consumption has finished. | |
* | |
* @param purchase The purchase that was (or was to be) consumed. | |
* @param result The result of the consumption operation. | |
*/ | |
public void onConsumeFinished(Purchase purchase, IabResult result); | |
} | |
/** | |
* Callback that notifies when a multi-item consumption operation finishes. | |
*/ | |
public interface OnConsumeMultiFinishedListener { | |
/** | |
* Called to notify that a consumption of multiple items has finished. | |
* | |
* @param purchases The purchases that were (or were to be) consumed. | |
* @param results The results of each consumption operation, corresponding to each | |
* sku. | |
*/ | |
public void onConsumeMultiFinished(List<Purchase> purchases, List<IabResult> results); | |
} | |
/** | |
* Asynchronous wrapper to item consumption. Works like {@link #consume}, but | |
* performs the consumption in the background and notifies completion through | |
* the provided listener. This method is safe to call from a UI thread. | |
* | |
* @param purchase The purchase to be consumed. | |
* @param listener The listener to notify when the consumption operation finishes. | |
*/ | |
public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) { | |
checkSetupDone("consume"); | |
List<Purchase> purchases = new ArrayList<Purchase>(); | |
purchases.add(purchase); | |
consumeAsyncInternal(purchases, listener, null); | |
} | |
/** | |
* Same as {@link consumeAsync}, but for multiple items at once. | |
* @param purchases The list of PurchaseInfo objects representing the purchases to consume. | |
* @param listener The listener to notify when the consumption operation finishes. | |
*/ | |
public void consumeAsync(List<Purchase> purchases, OnConsumeMultiFinishedListener listener) { | |
checkSetupDone("consume"); | |
consumeAsyncInternal(purchases, null, listener); | |
} | |
/** | |
* Returns a human-readable description for the given response code. | |
* | |
* @param code The response code | |
* @return A human-readable string explaining the result code. | |
* It also includes the result code numerically. | |
*/ | |
public static String getResponseDesc(int code) { | |
String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" + | |
"3:Billing Unavailable/4:Item unavailable/" + | |
"5:Developer Error/6:Error/7:Item Already Owned/" + | |
"8:Item not owned").split("/"); | |
String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" + | |
"-1002:Bad response received/" + | |
"-1003:Purchase signature verification failed/" + | |
"-1004:Send intent failed/" + | |
"-1005:User cancelled/" + | |
"-1006:Unknown purchase response/" + | |
"-1007:Missing token/" + | |
"-1008:Unknown error").split("/"); | |
if (code <= IABHELPER_ERROR_BASE) { | |
int index = IABHELPER_ERROR_BASE - code; | |
if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index]; | |
else return String.valueOf(code) + ":Unknown IAB Helper Error"; | |
} | |
else if (code < 0 || code >= iab_msgs.length) | |
return String.valueOf(code) + ":Unknown"; | |
else | |
return iab_msgs[code]; | |
} | |
// Checks that setup was done; if not, throws an exception. | |
void checkSetupDone(String operation) { | |
if (!mSetupDone) { | |
logError("Illegal state for operation (" + operation + "): IAB helper is not set up."); | |
throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation); | |
} | |
} | |
// Workaround to bug where sometimes response codes come as Long instead of Integer | |
int getResponseCodeFromBundle(Bundle b) { | |
Object o = b.get(RESPONSE_CODE); | |
if (o == null) { | |
logDebug("Bundle with null response code, assuming OK (known issue)"); | |
return BILLING_RESPONSE_RESULT_OK; | |
} | |
else if (o instanceof Integer) return ((Integer)o).intValue(); | |
else if (o instanceof Long) return (int)((Long)o).longValue(); | |
else { | |
logError("Unexpected type for bundle response code."); | |
logError(o.getClass().getName()); | |
throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName()); | |
} | |
} | |
// Workaround to bug where sometimes response codes come as Long instead of Integer | |
int getResponseCodeFromIntent(Intent i) { | |
Object o = i.getExtras().get(RESPONSE_CODE); | |
if (o == null) { | |
logError("Intent with no response code, assuming OK (known issue)"); | |
return BILLING_RESPONSE_RESULT_OK; | |
} | |
else if (o instanceof Integer) return ((Integer)o).intValue(); | |
else if (o instanceof Long) return (int)((Long)o).longValue(); | |
else { | |
logError("Unexpected type for intent response code."); | |
logError(o.getClass().getName()); | |
throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName()); | |
} | |
} | |
void flagStartAsync(String operation) { | |
if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" + | |
operation + ") because another async operation(" + mAsyncOperation + ") is in progress."); | |
mAsyncOperation = operation; | |
mAsyncInProgress = true; | |
logDebug("Starting async operation: " + operation); | |
} | |
void flagEndAsync() { | |
logDebug("Ending async operation: " + mAsyncOperation); | |
mAsyncOperation = ""; | |
mAsyncInProgress = false; | |
} | |
int queryPurchases(Inventory inv) throws JSONException, RemoteException { | |
// Query purchases | |
logDebug("Querying owned items..."); | |
logDebug("Package name: " + mContext.getPackageName()); | |
boolean hasMore = true; | |
boolean verificationFailed = false; | |
String continueToken = null; | |
do { | |
logDebug("Calling getPurchases with continuation token: " + continueToken); | |
Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(), | |
ITEM_TYPE_INAPP, continueToken); | |
int response = getResponseCodeFromBundle(ownedItems); | |
logDebug("Owned items response: " + String.valueOf(response)); | |
if (response != BILLING_RESPONSE_RESULT_OK) { | |
logDebug("getPurchases() failed: " + getResponseDesc(response)); | |
return response; | |
} | |
if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST) | |
|| !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST) | |
|| !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) { | |
logError("Bundle returned from getPurchases() doesn't contain required fields."); | |
return IABHELPER_BAD_RESPONSE; | |
} | |
ArrayList<String> ownedSkus = ownedItems.getStringArrayList( | |
RESPONSE_INAPP_ITEM_LIST); | |
ArrayList<String> purchaseDataList = ownedItems.getStringArrayList( | |
RESPONSE_INAPP_PURCHASE_DATA_LIST); | |
ArrayList<String> signatureList = ownedItems.getStringArrayList( | |
RESPONSE_INAPP_SIGNATURE_LIST); | |
for (int i = 0; i < purchaseDataList.size(); ++i) { | |
String purchaseData = purchaseDataList.get(i); | |
String signature = signatureList.get(i); | |
String sku = ownedSkus.get(i); | |
if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) { | |
logDebug("Sku is owned: " + sku); | |
Purchase purchase = new Purchase(purchaseData, signature); | |
if (TextUtils.isEmpty(purchase.getToken())) { | |
logWarn("BUG: empty/null token!"); | |
logDebug("Purchase data: " + purchaseData); | |
} | |
// Record ownership and token | |
inv.addPurchase(purchase); | |
} | |
else { | |
logWarn("Purchase signature verification **FAILED**. Not adding item."); | |
logDebug(" Purchase data: " + purchaseData); | |
logDebug(" Signature: " + signature); | |
verificationFailed = true; | |
} | |
} | |
continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN); | |
logDebug("Continuation token: " + continueToken); | |
} while (!TextUtils.isEmpty(continueToken)); | |
return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK; | |
} | |
int querySkuDetails(Inventory inv, List<String> moreSkus) throws RemoteException, JSONException { | |
logDebug("Querying SKU details."); | |
ArrayList<String> skuList = new ArrayList<String>(); | |
skuList.addAll(inv.getAllOwnedSkus()); | |
if (moreSkus != null) skuList.addAll(moreSkus); | |
if (skuList.size() == 0) { | |
logDebug("queryPrices: nothing to do because there are no SKUs."); | |
return BILLING_RESPONSE_RESULT_OK; | |
} | |
Bundle querySkus = new Bundle(); | |
querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList); | |
Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(), | |
ITEM_TYPE_INAPP, querySkus); | |
if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) { | |
int response = getResponseCodeFromBundle(skuDetails); | |
if (response != BILLING_RESPONSE_RESULT_OK) { | |
logDebug("getSkuDetails() failed: " + getResponseDesc(response)); | |
return response; | |
} | |
else { | |
logError("getSkuDetails() returned a bundle with neither an error nor a detail list."); | |
return IABHELPER_BAD_RESPONSE; | |
} | |
} | |
ArrayList<String> responseList = skuDetails.getStringArrayList( | |
RESPONSE_GET_SKU_DETAILS_LIST); | |
for (String thisResponse : responseList) { | |
SkuDetails d = new SkuDetails(thisResponse); | |
logDebug("Got sku details: " + d); | |
inv.addSkuDetails(d); | |
} | |
return BILLING_RESPONSE_RESULT_OK; | |
} | |
void consumeAsyncInternal(final List<Purchase> purchases, | |
final OnConsumeFinishedListener singleListener, | |
final OnConsumeMultiFinishedListener multiListener) { | |
final Handler handler = new Handler(); | |
flagStartAsync("consume"); | |
(new Thread(new Runnable() { | |
public void run() { | |
final List<IabResult> results = new ArrayList<IabResult>(); | |
for (Purchase purchase : purchases) { | |
try { | |
consume(purchase); | |
results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku())); | |
} | |
catch (IabException ex) { | |
results.add(ex.getResult()); | |
} | |
} | |
flagEndAsync(); | |
if (singleListener != null) { | |
handler.post(new Runnable() { | |
public void run() { | |
singleListener.onConsumeFinished(purchases.get(0), results.get(0)); | |
} | |
}); | |
} | |
if (multiListener != null) { | |
handler.post(new Runnable() { | |
public void run() { | |
multiListener.onConsumeMultiFinished(purchases, results); | |
} | |
}); | |
} | |
} | |
})).start(); | |
} | |
void logDebug(String msg) { | |
if (mDebugLog) Log.d(mDebugTag, msg); | |
} | |
void logError(String msg) { | |
Log.e(mDebugTag, "In-app billing error: " + msg); | |
} | |
void logWarn(String msg) { | |
Log.w(mDebugTag, "In-app billing warning: " + msg); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Yigit - Is this supposed to be a replacement code in the IabHelper class provided in the TrivialDrive sample by Google? Any additional features / mechanisms baked in here?