com.github.jberkel.pay.me.IabHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.github.jberkel.pay.me.IabHelper.java

Source

/* 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);
                }
            }
        }
    }
}