/* 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.github.jberkel.pay.me; 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.IBinder; import android.os.RemoteException; import android.text.TextUtils; import android.util.Log; import com.android.vending.billing.IInAppBillingService; import com.github.jberkel.pay.me.listener.OnConsumeFinishedListener; import com.github.jberkel.pay.me.listener.OnConsumeMultiFinishedListener; import com.github.jberkel.pay.me.listener.OnIabPurchaseFinishedListener; import com.github.jberkel.pay.me.listener.OnIabSetupFinishedListener; import com.github.jberkel.pay.me.listener.QueryInventoryFinishedListener; import com.github.jberkel.pay.me.model.Inventory; import com.github.jberkel.pay.me.model.ItemType; import com.github.jberkel.pay.me.model.Purchase; import com.github.jberkel.pay.me.model.SkuDetails; import com.github.jberkel.pay.me.validator.DefaultSignatureValidator; import com.github.jberkel.pay.me.validator.SignatureValidator; import org.json.JSONException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static android.app.Activity.RESULT_CANCELED; import static android.app.Activity.RESULT_OK; import static com.github.jberkel.pay.me.IabConsts.*; import static com.github.jberkel.pay.me.Response.*; import static com.github.jberkel.pay.me.model.ItemType.INAPP; import static com.github.jberkel.pay.me.model.ItemType.SUBS; /** * Provides convenience methods for in-app billing. You can 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. * <p/> * 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. * <p/> * After setup is complete, you will typically want to request an inventory of owned * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync} * and related methods. * <p/> * 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. * <p/> * 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) * @author Jan Berkel */ public class IabHelper { /* package */ static final Intent BIND_BILLING_SERVICE = new Intent("com.android.vending.billing.InAppBillingService.BIND"); private Context mContext; private IInAppBillingService mService; private BillingServiceConnection mServiceConn; private PurchaseFlowState mPurchaseFlowState = PurchaseFlowState.NONE; private SignatureValidator mSignatureValidator; private boolean mSetupDone, mDisposed; private boolean mSubscriptionsSupported, mInAppSupported; private boolean mDebugLog; private String mDebugTag = "IabHelper"; private boolean mAsyncInProgress; // if mAsyncInProgress == true, what asynchronous operation is in progress? (for logging/debugging) private String mAsyncOperation = ""; /** * 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) { this(ctx, new DefaultSignatureValidator(base64PublicKey)); } /** * Alternative constructor if you want to perform your own signature validation by * implementing a {@link SignatureValidator}. * * @param ctx Your application or Activity context. Needed to bind to the in-app billing service. * @param validator the validator to be used for verifying the signature. */ public IabHelper(Context ctx, SignatureValidator validator) { if (ctx == null) throw new IllegalArgumentException("need non-null context"); if (validator == null) throw new IllegalArgumentException("need non-null validator"); mContext = ctx.getApplicationContext(); mSignatureValidator = validator; logDebug("IAB helper created."); } /** * 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) { checkNotDisposedAndThrow(); if (mSetupDone) throw new IllegalStateException("IAB helper is already set up."); logDebug("Starting in-app billing setup."); if (!mContext.getPackageManager().queryIntentServices(BIND_BILLING_SERVICE, 0).isEmpty()) { // service available to handle that Intent mServiceConn = new BillingServiceConnection(listener); if (!mContext.bindService(BIND_BILLING_SERVICE, mServiceConn, Context.BIND_AUTO_CREATE)) { logWarn("Could not bind to service"); } } else { // no service available to handle that Intent mSetupDone = true; if (listener != null) { listener.onIabSetupFinished(new IabResult(BILLING_UNAVAILABLE)); } } } /** * 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); } mDisposed = true; mContext = null; mServiceConn = null; mService = null; mPurchaseFlowState = PurchaseFlowState.NONE; } /** * 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 activity The calling activity. * @param sku The sku of the item to purchase. * @param itemType indicates if it's a product or a subscription * @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, can be null * @param developerPayload 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(final Activity activity, final String sku, final ItemType itemType, final int requestCode, final OnIabPurchaseFinishedListener listener, final String developerPayload) { if (TextUtils.isEmpty(sku)) throw new IllegalArgumentException("Empty sku"); if (itemType == null) throw new IllegalArgumentException("Empty itemType"); if (isDisposed()) { if (listener != null) { listener.onIabPurchaseFinished(new IabResult(Response.IABHELPER_DISPOSED), null); } return; } checkSetupDone("launchPurchaseFlow"); flagStartAsync("launchPurchaseFlow"); if (itemType == SUBS && !mSubscriptionsSupported || itemType == INAPP && !mInAppSupported) { flagEndAsync(); if (listener != null) { listener.onIabPurchaseFinished( new IabResult(itemType == INAPP ? BILLING_UNAVAILABLE : IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE), null); } return; } try { logDebug("Constructing buy intent for " + sku + ", item type: " + itemType); Bundle buyIntentBundle = mService.getBuyIntent(API_VERSION, mContext.getPackageName(), sku, itemType.toString(), developerPayload); int response = getResponseCodeFromBundle(buyIntentBundle); if (response != OK.code) { logError("Unable to buy item, Error response: " + getDescription(response)); flagEndAsync(); if (listener != null) { listener.onIabPurchaseFinished( new IabResult(response, "Unable to buy item"), null); } return; } PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); if (pendingIntent == null) { throw new SendIntentException("No pending intent"); } logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode); mPurchaseFlowState = new PurchaseFlowState(requestCode, itemType, listener); activity.startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, new Intent(), 0, 0, 0); } catch (SendIntentException e) { logError("SendIntentException while launching purchase flow for sku " + sku, e); flagEndAsync(); if (listener != null) listener.onIabPurchaseFinished(new IabResult(IABHELPER_SEND_INTENT_FAILED), null); } catch (RemoteException e) { logError("RemoteException while launching purchase flow for sku " + sku, e); flagEndAsync(); if (listener != null) listener.onIabPurchaseFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION), 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 Activity#onActivityResult} method. This method * MUST be called from the UI thread of the Activity. * * @param requestCode The requestCode as you received it. * @param intentResultCode The resultCode as you received it. * @param intent 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 intentResultCode, Intent intent) { if (mPurchaseFlowState == PurchaseFlowState.NONE) return false; // no prior launchPurchaseFlow else if (requestCode != mPurchaseFlowState.requestCode) return false; checkSetupDone("handleActivityResult"); // end of async purchase operation that started on launchPurchaseFlow flagEndAsync(); if (intent == null) { logError("Null data in IAB activity result."); mPurchaseFlowState.onIabPurchaseFinished(new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result"), null); return true; } else { int responseCode = getResponseCodeFromBundle(intent.getExtras()); logDebug("handleActivityResult: resultCode="+intentResultCode+", response code="+responseCode); if (intentResultCode == RESULT_OK) { if (responseCode == OK.code) { handlePurchaseResult( intent.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA), intent.getStringExtra(RESPONSE_INAPP_SIGNATURE), mPurchaseFlowState); } else { logDebug("Result code was OK but in-app billing response was not OK: " + getDescription(responseCode)); mPurchaseFlowState.onIabPurchaseFinished( new IabResult(responseCode, "Problem purchasing item."), null); } return true; } else if (intentResultCode == RESULT_CANCELED) { logDebug("Purchase canceled - Response: " + getDescription(responseCode)); mPurchaseFlowState.onIabPurchaseFinished(new IabResult(responseCode, null), null); return true; } else { // weird intent result code logError("Purchase failed. Result code: " + Integer.toString(intentResultCode) + ". Response: " + getDescription(responseCode)); mPurchaseFlowState.onIabPurchaseFinished(new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE), null); return false; } } } /** * 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 #queryInventoryAsync(boolean, List, List, QueryInventoryFinishedListener)}. * * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well * as purchase information. * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership. * Ignored if null or if querySkuDetails is false. * @param moreSubsSkus additional SUBSCRIPTIONS 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> moreItemSkus, List<String> moreSubsSkus) throws IabException { checkNotDisposed(); checkSetupDone("queryInventory"); try { final Inventory inventory = new Inventory(); queryPurchasesAndDetails(INAPP, inventory, querySkuDetails, moreItemSkus); if (subscriptionsSupported()) { queryPurchasesAndDetails(SUBS, inventory, querySkuDetails, moreSubsSkus); } return inventory; } 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); } } /** * 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 List<String> moreSubSkus, final QueryInventoryFinishedListener listener) { if (isDisposed()) { if (listener != null) { listener.onQueryInventoryFinished(new IabResult(Response.IABHELPER_DISPOSED), null); } return; } checkSetupDone("queryInventory"); new QueryInventoryTask(this, listener).execute(new QueryInventoryTask.Args(querySkuDetails, moreSkus, moreSubSkus)); } /** * Convenience method which queries for all purchased items, including details. * See {@link #queryInventoryAsync(boolean, java.util.List, java.util.List, QueryInventoryFinishedListener)} * @param listener The listener to notify when the refresh operation completes. */ public void queryInventoryAsync(final QueryInventoryFinishedListener listener) { queryInventoryAsync(true, null, 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 purchase The PurchaseInfo that represents the item to consume. * @throws IabException if there is a problem during consumption. */ public void consume(Purchase purchase) throws IabException { checkNotDisposed(); checkSetupDone("consume"); if (purchase.getItemType() != INAPP) { throw new IabException(IABHELPER_INVALID_CONSUMPTION, "Items of type '" + purchase.getItemType() + "' can't be consumed."); } try { String token = purchase.getToken(); String sku = purchase.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 + " " + purchase); } logDebug("Consuming sku: " + sku + ", token: " + token); int response = mService.consumePurchase(API_VERSION, mContext.getPackageName(), token); if (response == OK.code) { logDebug("Successfully consumed sku: " + sku); } else { logDebug("Error consuming consuming sku " + sku + ". " + getDescription(response)); throw new IabException(response, "Error consuming sku " + sku); } } catch (RemoteException e) { throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + purchase, e); } } /** * 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) { if (isDisposed()) { if (listener != null) listener.onConsumeFinished(null, new IabResult(IABHELPER_DISPOSED)); return; } checkSetupDone("consume"); consumeAsyncInternal(Arrays.asList(purchase), listener, null); } /** * Same as {@link #consumeAsync(Purchase, OnConsumeFinishedListener)}, 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) { if (isDisposed()) { if (listener != null) { listener.onConsumeMultiFinished(new ArrayList<Purchase>(), new ArrayList<IabResult>()); } return; } checkSetupDone("consume"); consumeAsyncInternal(purchases, null, listener); } /** * Returns whether subscriptions are supported. */ public boolean subscriptionsSupported() { return !isDisposed() && mSubscriptionsSupported; } /** * Enables or disable debug logging through LogCat. */ public void enableDebugLogging(boolean enable, String tag) { checkNotDisposedAndThrow(); enableDebugLogging(enable); mDebugTag = tag; } public void enableDebugLogging(boolean enable) { checkNotDisposedAndThrow(); mDebugLog = enable; } // Checks that setup was done; if not, throws an exception. /* package */ 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); } } /* package */ int getResponseCodeFromBundle(Bundle bundle) { Object o; if (bundle == null || (o = bundle.get(RESPONSE_CODE)) == null) { logDebug("Bundle with null response code, assuming OK (known issue)"); return OK.code; } else if (o instanceof Integer) return (Integer) o; // Workaround to bug where sometimes response codes come as Long instead of Integer 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()); } } /* package */ 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); } /* package */ void flagEndAsync() { logDebug("Ending async operation: " + mAsyncOperation); mAsyncOperation = ""; mAsyncInProgress = false; } /* package */ boolean isDisposed() { return mDisposed; } private void checkNotDisposed() throws IabException { if (isDisposed()) { throw new IabException(IABHELPER_DISPOSED, null); } } private void checkNotDisposedAndThrow() throws IllegalStateException { if (isDisposed()) { throw new IllegalStateException("IabHelper was disposed of, so it cannot be used."); } } private void handlePurchaseResult(String purchaseData, String dataSignature, PurchaseFlowState purchaseState) { if (purchaseData == null || dataSignature == null) { logError("BUG: either purchaseData or dataSignature is null." + " data="+purchaseData+", signature="+dataSignature); purchaseState.onIabPurchaseFinished( new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature"), null); return; } try { Purchase purchase = new Purchase(purchaseState.itemType, purchaseData, dataSignature); if (!mSignatureValidator.validate(purchaseData, dataSignature)) { logError("Purchase signature verification FAILED for " + purchase); purchaseState.onIabPurchaseFinished( new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for purchase " + purchase), purchase); return; } logDebug("Purchase signature successfully verified."); purchaseState.onIabPurchaseFinished(new IabResult(OK), purchase); } catch (JSONException e) { logError("Failed to parse purchase data.", e); purchaseState.onIabPurchaseFinished( new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data."), null); } } private void queryPurchasesAndDetails(ItemType itemType, Inventory inventory, boolean queryDetails, List<String> extraSkus) throws JSONException, RemoteException, IabException { checkNotDisposed(); int result = queryPurchases(inventory, itemType); if (result != OK.code) { throw new IabException(result, "Error querying purchases for "+itemType); } if (queryDetails) { result = querySkuDetails(itemType, inventory, extraSkus); if (result != OK.code) { throw new IabException(result, "Error querying purchase details for "+itemType); } } } private int queryPurchases(Inventory inv, ItemType itemType) throws JSONException, RemoteException, IabException { checkNotDisposed(); logDebug("Querying owned items, item type: " + itemType); boolean verificationFailed = false; String continueToken = null; do { Bundle ownedItems = mService.getPurchases(API_VERSION, mContext.getPackageName(), itemType.toString(), continueToken); int response = getResponseCodeFromBundle(ownedItems); if (response != OK.code) { logDebug("getPurchases() failed: " + getDescription(response)); return response; } if (ownedItems == null || !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.code; } List<String> ownedSkus = ownedItems.getStringArrayList(RESPONSE_INAPP_ITEM_LIST); List<String> purchaseDataList = ownedItems.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST); List<String> signatureList = ownedItems.getStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST); if (signatureList.size() < purchaseDataList.size() || ownedSkus.size() < purchaseDataList.size()) { logError("invalid data returned by service"); return ERROR.code; } for (int i = 0; i < purchaseDataList.size(); i++) { String purchaseData = purchaseDataList.get(i); String signature = signatureList.get(i); if (mSignatureValidator.validate(purchaseData, signature)) { Purchase purchase = new Purchase(itemType, purchaseData, signature); if (TextUtils.isEmpty(purchase.getToken())) { logWarn("BUG: empty/null token!"); logDebug("Purchase data: " + purchaseData); } 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.code : OK.code; } private int querySkuDetails(ItemType itemType, Inventory inv, List<String> moreSkus) throws RemoteException, JSONException { logDebug("Querying SKU details."); ArrayList<String> skuList = new ArrayList<String>(); skuList.addAll(inv.getAllOwnedSkus(itemType)); if (moreSkus != null) { for (String sku : moreSkus) { if (!skuList.contains(sku)) { skuList.add(sku); } } } if (skuList.isEmpty()) { logDebug("querySkuDetails: nothing to do because there are no SKUs."); return OK.code; } // TODO: check for 20 SKU limit + add batching ? Bundle querySkus = new Bundle(); querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList); Bundle skuDetails = mService.getSkuDetails(API_VERSION, mContext.getPackageName(), itemType.toString(), querySkus); if (skuDetails == null) return IABHELPER_BAD_RESPONSE.code; if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) { int response = getResponseCodeFromBundle(skuDetails); if (response != OK.code) { logWarn("getSkuDetails() failed: " + getDescription(response)); return response; } else { logError("getSkuDetails() returned a bundle with neither an error nor a detail list."); return IABHELPER_BAD_RESPONSE.code; } } ArrayList<String> responseList = skuDetails.getStringArrayList(RESPONSE_GET_SKU_DETAILS_LIST); for (String json : responseList) { inv.addSkuDetails(new SkuDetails(json)); } return OK.code; } private ConsumeTask consumeAsyncInternal(final List<Purchase> purchases, final OnConsumeFinishedListener singleListener, final OnConsumeMultiFinishedListener multiListener) { return (ConsumeTask) new ConsumeTask(this, singleListener, multiListener).execute(purchases.toArray(new Purchase[purchases.size()])); } private void logDebug(String msg) { if (mDebugLog) Log.d(mDebugTag, msg); } private void logError(String msg) { logError(msg, null); } private void logError(String msg, Throwable t) { Log.e(mDebugTag, "In-app billing error: " + msg, t); } private void logWarn(String msg) { Log.w(mDebugTag, "In-app billing warning: " + msg); } // for testing /* package */ IInAppBillingService getInAppBillingService(IBinder service) { return IInAppBillingService.Stub.asInterface(service); } private class BillingServiceConnection implements ServiceConnection { private OnIabSetupFinishedListener listener; BillingServiceConnection(OnIabSetupFinishedListener listener) { this.listener = listener; } @Override public void onServiceDisconnected(ComponentName name) { logDebug("Billing service disconnected."); mService = null; listener = null; } @Override public void onServiceConnected(ComponentName name, IBinder service) { if (isDisposed()) return; logDebug("Billing service connected."); mService = getInAppBillingService(service); String packageName = mContext.getPackageName(); IabResult result = new IabResult(OK); try { logDebug("Checking for in-app billing 3 support."); int response = mService.isBillingSupported(API_VERSION, packageName, INAPP.toString()); if (response == OK.code) { logDebug("In-app billing version 3 supported for " + packageName); mInAppSupported = true; logDebug("Checking for in-app billing 3 subscription support."); response = mService.isBillingSupported(API_VERSION, packageName, SUBS.toString()); if (response == OK.code) { logDebug("Subscriptions AVAILABLE."); mSubscriptionsSupported = true; } else { logDebug("Subscriptions NOT AVAILABLE. Response: " + response + " " + Response.getDescription(response)); } } else { result = new IabResult(response, null); } } catch (RemoteException e) { result = new IabResult(IABHELPER_REMOTE_EXCEPTION); Log.e(mDebugTag, "RemoteException while setting up in-app billing.", e); } finally { mSetupDone = true; if (listener != null) { listener.onIabSetupFinished(result); } } } } }