package com.android.vending.billing; import java.util.ArrayList; import java.util.List; 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.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import org.json.JSONException; /** * 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) */ public class IabHelper { // JAVADOC:OFF private static final String OPERATION_CONSUME = "consume"; // Is debug logging enabled? private boolean mDebugLog = false; private String mDebugTag = "IabHelper"; // Is setup done? private boolean mSetupDone = false; // Has this object been disposed of? (If so, we should ignore callbacks, etc) private boolean mDisposed = false; // Are subscriptions supported? private boolean mSubscriptionsSupported = false; // Is an asynchronous operation in progress? // (only one at a time can be in progress) private boolean mAsyncInProgress = false; // (for logging/debugging) // if mAsyncInProgress == true, what asynchronous operation is in progress? private String mAsyncOperation = ""; // Context we were passed during initialization @Nullable private Context mContext; // Connection to the service @Nullable private IInAppBillingService mService; @Nullable private ServiceConnection mServiceConn; // The request code used to launch purchase flow private int mRequestCode; // The item type of the current purchase flow private String mPurchasingItemType; // Public key for verifying signature, in base64 encoding @Nullable private 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; private 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 private static final int IABHELPER_ERROR_BASE = -1000; private static final int IABHELPER_REMOTE_EXCEPTION = -1001; private static final int IABHELPER_BAD_RESPONSE = -1002; private static final int IABHELPER_VERIFICATION_FAILED = -1003; private static final int IABHELPER_SEND_INTENT_FAILED = -1004; private static final int IABHELPER_USER_CANCELLED = -1005; private static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006; private static final int IABHELPER_MISSING_TOKEN = -1007; private static final int IABHELPER_UNKNOWN_ERROR = -1008; private static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009; private static final int IABHELPER_INVALID_CONSUMPTION = -1010; // Keys for the responses from InAppBillingService private static final String RESPONSE_CODE = "RESPONSE_CODE"; private static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST"; private static final String RESPONSE_BUY_INTENT = "BUY_INTENT"; private static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA"; private static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE"; private static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST"; private static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST"; private static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST"; private static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN"; // Item types public static final String ITEM_TYPE_INAPP = "inapp"; private static final String ITEM_TYPE_SUBS = "subs"; // some fields on the getSkuDetails response bundle private static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST"; public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST"; private static final int API_VERSION = 3; // JAVADOC:ON /** * 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(@NonNull final Context ctx, final String base64PublicKey) { mContext = ctx.getApplicationContext(); mSignatureBase64 = base64PublicKey; logDebug("IAB helper created."); } /** * Enables or disable debug logging through LogCat. * * @param enable Flag indicating if logging should be enabled. * @param tag The logging tag. */ public final void enableDebugLogging(final boolean enable, final String tag) { checkNotDisposed(); mDebugLog = enable; mDebugTag = tag; } /** * Enable or disable debug logging. * * @param enable Flag indicating if logging should be enabled. */ public final void enableDebugLogging(final boolean enable) { checkNotDisposed(); mDebugLog = enable; } /** * 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 final void startSetup(@Nullable final OnIabSetupFinishedListener listener) { // If already set up, can't do it again. checkNotDisposed(); if (mSetupDone) { throw new IllegalStateException("IAB helper is already set up."); } // Connection to IAB service logDebug("Starting in-app billing setup."); mServiceConn = new ServiceConnection() { @Override public void onServiceDisconnected(final ComponentName name) { logDebug("Billing service disconnected."); mService = null; } @Override public void onServiceConnected(final ComponentName name, final IBinder service) { if (mDisposed) { return; } logDebug("Billing service connected."); mService = IInAppBillingService.Stub.asInterface(service); String packageName = mContext.getPackageName(); try { logDebug("Checking for in-app billing 3 support."); // check for in-app billing v3 support int response = mService.isBillingSupported(API_VERSION, packageName, ITEM_TYPE_INAPP); if (response != BILLING_RESPONSE_RESULT_OK) { if (listener != null) { listener.onIabSetupFinished(new IabResult(response, "Error checking for billing v3 support.")); } // if in-app purchases aren't supported, neither are subscriptions. mSubscriptionsSupported = false; return; } logDebug("In-app billing version 3 supported for " + packageName); // check for v3 subscriptions support response = mService.isBillingSupported(API_VERSION, packageName, ITEM_TYPE_SUBS); if (response == BILLING_RESPONSE_RESULT_OK) { logDebug("Subscriptions AVAILABLE."); mSubscriptionsSupported = true; } else { logDebug("Subscriptions NOT AVAILABLE. Response: " + response); } mSetupDone = true; } catch (RemoteException e) { if (listener != null) { listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION, "RemoteException while setting up in-app billing.")); } e.printStackTrace(); return; } if (listener != null) { listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful.")); } } }; Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); serviceIntent.setPackage("com.android.vending"); if (mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) { // no service available to handle that Intent if (listener != null) { listener.onIabSetupFinished( new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, "Billing service unavailable on device.")); } } else { // service available to handle that Intent mContext.bindService(serviceIntent, 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 final void dispose() { logDebug("Disposing."); mSetupDone = false; if (mServiceConn != null) { logDebug("Unbinding from service."); if (mContext != null && mService != null) { mContext.unbindService(mServiceConn); } } mDisposed = true; mContext = null; mServiceConn = null; mService = null; mPurchaseListener = null; } /** * Throw exception if IabHelper was disposed. */ private void checkNotDisposed() { if (mDisposed) { throw new IllegalStateException("IabHelper was disposed of, so it cannot be used."); } } /** * Returns whether subscriptions are supported. * * @return true if subscriptions are supported. */ public final boolean subscriptionsSupported() { checkNotDisposed(); return mSubscriptionsSupported; } /** * The listener registered on launchPurchaseFlow, which we have to call back when the purchase finishes. */ @Nullable private OnIabPurchaseFinishedListener mPurchaseListener; public final void launchPurchaseFlow(@NonNull final Activity act, final String sku, final int requestCode, final OnIabPurchaseFinishedListener listener) { launchPurchaseFlow(act, sku, requestCode, listener, ""); } private void launchPurchaseFlow(@NonNull final Activity act, final String sku, final int requestCode, final OnIabPurchaseFinishedListener listener, final String extraData) { launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData); } public final void launchSubscriptionPurchaseFlow(@NonNull final Activity act, final String sku, final int requestCode, final OnIabPurchaseFinishedListener listener) { launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, ""); } private void launchSubscriptionPurchaseFlow(@NonNull final Activity act, final String sku, final int requestCode, final OnIabPurchaseFinishedListener listener, final String extraData) { launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData); } /** * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase, which will involve * bringing up the Google Play screen. The calling activity will be paused while the user interacts with Google * Play, and the result will be delivered via the activity's {@link android.app.Activity#onActivityResult} method, * at which point you must call this object's {@link #handleActivityResult} method to continue the purchase flow. * This method MUST be called from the UI thread of the Activity. * * @param act The calling activity. * @param sku The sku of the item to purchase. * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS) * @param requestCode A request code (to differentiate from other responses -- as in * {@link android.app.Activity#startActivityForResult}). * @param listener The listener to notify when the purchase process finishes * @param extraData Extra data (developer payload), which will be returned with the purchase data when the purchase * completes. This extra data will be permanently bound to that purchase and will always be returned when * the purchase is queried. */ private void launchPurchaseFlow(@NonNull final Activity act, final String sku, @NonNull final String itemType, final int requestCode, @Nullable final OnIabPurchaseFinishedListener listener, final String extraData) { checkNotDisposed(); checkSetupDone("launchPurchaseFlow"); flagStartAsync("launchPurchaseFlow"); IabResult result; if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) { IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE, "Subscriptions are not available."); flagEndAsync(); if (listener != null) { listener.onIabPurchaseFinished(r, null); } return; } try { logDebug("Constructing buy intent for " + sku + ", item type: " + itemType); Bundle buyIntentBundle = mService.getBuyIntent(API_VERSION, mContext.getPackageName(), sku, itemType, extraData); int response = getResponseCodeFromBundle(buyIntentBundle); if (response != BILLING_RESPONSE_RESULT_OK) { logError("Unable to buy item, Error response: " + getResponseDesc(response)); flagEndAsync(); result = new IabResult(response, "Unable to buy item"); if (listener != null) { listener.onIabPurchaseFinished(result, null); } return; } logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode); mRequestCode = requestCode; mPurchaseListener = listener; mPurchasingItemType = itemType; PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); if (pendingIntent != null) { act.startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, new Intent(), 0, 0, 0); } } catch (SendIntentException e) { logError("SendIntentException while launching purchase flow for sku " + sku); e.printStackTrace(); flagEndAsync(); result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent."); if (listener != null) { listener.onIabPurchaseFinished(result, null); } } catch (RemoteException e) { logError("RemoteException while launching purchase flow for sku " + sku); e.printStackTrace(); flagEndAsync(); result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow"); if (listener != null) { listener.onIabPurchaseFinished(result, null); } } } /** * 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 final boolean handleActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) { logDebug("Got activity result for " + requestCode + ". Expected: " + mRequestCode); IabResult result; if (requestCode != mRequestCode) { return false; } checkNotDisposed(); checkSetupDone("handleActivityResult"); // end of async purchase operation that started on launchPurchaseFlow flagEndAsync(); if (data == null) { logError("Null data in IAB activity result."); result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result"); if (mPurchaseListener != null) { mPurchaseListener.onIabPurchaseFinished(result, null); } return true; } int responseCode = getResponseCodeFromIntent(data); String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA); String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE); if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) { logDebug("Successful resultcode from purchase activity."); logDebug("Purchase data: " + purchaseData); logDebug("Data signature: " + dataSignature); logDebug("Extras: " + data.getExtras()); logDebug("Expected item type: " + mPurchasingItemType); if (purchaseData == null || dataSignature == null) { logError("BUG: either purchaseData or dataSignature is null."); logDebug("Extras: " + data.getExtras().toString()); result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature"); if (mPurchaseListener != null) { mPurchaseListener.onIabPurchaseFinished(result, null); } return true; } Purchase purchase; try { purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature); String sku = purchase.getSku(); // Verify signature if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) { logError("Purchase signature verification FAILED for sku " + sku); result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku); if (mPurchaseListener != null) { mPurchaseListener.onIabPurchaseFinished(result, purchase); } return true; } logDebug("Purchase signature successfully verified."); } catch (JSONException e) { logError("Failed to parse purchase data."); e.printStackTrace(); result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data."); if (mPurchaseListener != null) { mPurchaseListener.onIabPurchaseFinished(result, null); } return true; } if (mPurchaseListener != null) { mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase); } } else if (resultCode == Activity.RESULT_OK) { // result code was OK, but in-app billing response was not OK. logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode)); if (mPurchaseListener != null) { result = new IabResult(responseCode, "Problem purchashing item."); mPurchaseListener.onIabPurchaseFinished(result, null); } } else if (resultCode == Activity.RESULT_CANCELED) { logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode)); result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled."); if (mPurchaseListener != null) { mPurchaseListener.onIabPurchaseFinished(result, null); } } else { logError("Purchase failed. Result code: " + Integer.toString(resultCode) + ". Response: " + getResponseDesc(responseCode)); result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response."); if (mPurchaseListener != null) { mPurchaseListener.onIabPurchaseFinished(result, null); } } return true; } @NonNull private Inventory queryInventory(final boolean querySkuDetails, final List<String> moreSkus) throws IabException { return queryInventory(querySkuDetails, moreSkus, null); } /** * 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 refreshInventoryAsync. * * @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. * @return the inventory. * @throws IabException if a problem occurs while refreshing the inventory. */ @NonNull private Inventory queryInventory(final boolean querySkuDetails, final List<String> moreItemSkus, final List<String> moreSubsSkus) throws IabException { checkNotDisposed(); checkSetupDone("queryInventory"); try { Inventory inv = new Inventory(); int r = queryPurchases(inv, ITEM_TYPE_INAPP); if (r != BILLING_RESPONSE_RESULT_OK) { throw new IabException(r, "Error refreshing inventory (querying owned items)."); } if (querySkuDetails) { r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus); if (r != BILLING_RESPONSE_RESULT_OK) { throw new IabException(r, "Error refreshing inventory (querying prices of items)."); } } // if subscriptions are supported, then also query for subscriptions if (mSubscriptionsSupported) { r = queryPurchases(inv, ITEM_TYPE_SUBS); if (r != BILLING_RESPONSE_RESULT_OK) { throw new IabException(r, "Error refreshing inventory (querying owned subscriptions)."); } if (querySkuDetails) { r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus); if (r != BILLING_RESPONSE_RESULT_OK) { throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions)."); } } } 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); } } /** * 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 final void queryInventoryAsync(final boolean querySkuDetails, final List<String> moreSkus, @Nullable final QueryInventoryFinishedListener listener) { final Handler handler = new Handler(); checkNotDisposed(); checkSetupDone("queryInventory"); flagStartAsync("refresh inventory"); (new Thread(new Runnable() { @Override 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(); } catch (Exception e) { // NullPointerException may happen if activity is closed too quickly Log.e(mDebugTag, "Error when querying inventory", e); return; } flagEndAsync(); final IabResult resultF = result; final Inventory invF = inv; if (!mDisposed && listener != null) { handler.post(new Runnable() { @Override public void run() { listener.onQueryInventoryFinished(resultF, invF); } }); } } })).start(); } public final void queryInventoryAsync(final QueryInventoryFinishedListener listener) { queryInventoryAsync(true, null, listener); } public final void queryInventoryAsync(final boolean querySkuDetails, final 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. */ private void consume(@NonNull final Purchase itemInfo) throws IabException { checkNotDisposed(); checkSetupDone(OPERATION_CONSUME); if (!itemInfo.getItemType().equals(ITEM_TYPE_INAPP)) { throw new IabException(IABHELPER_INVALID_CONSUMPTION, "Items of type '" + itemInfo.getItemType() + "' can't be consumed."); } try { String token = itemInfo.getToken(); String sku = itemInfo.getSku(); if (token == null || "".equals(token)) { 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(API_VERSION, 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); } } /** * 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 final void consumeAsync(final Purchase purchase, final OnConsumeFinishedListener listener) { checkNotDisposed(); checkSetupDone(OPERATION_CONSUME); List<Purchase> purchases = new ArrayList<>(); purchases.add(purchase); consumeAsyncInternal(purchases, listener, null); } /** * Same as 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 final void consumeAsync(@NonNull final List<Purchase> purchases, final OnConsumeMultiFinishedListener listener) { checkNotDisposed(); checkSetupDone(OPERATION_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 final String getResponseDesc(final int code) { String[] iabMsgs = ("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[] iabhelperMsgs = ("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/" + "-1009:Subscriptions not available/" + "-1010:Invalid consumption attempt").split("/"); if (code <= IABHELPER_ERROR_BASE) { int index = IABHELPER_ERROR_BASE - code; if (index >= 0 && index < iabhelperMsgs.length) { return iabhelperMsgs[index]; } else { return String.valueOf(code) + ":Unknown IAB Helper Error"; } } else if (code < 0 || code >= iabMsgs.length) { return String.valueOf(code) + ":Unknown"; } else { return iabMsgs[code]; } } /** * Checks that setup was done; if not, throws an exception. * * @param operation The operation (passed to the exception). */ private void checkSetupDone(final 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. * * @param b The bundle. * @return the response code. */ private int getResponseCodeFromBundle(@NonNull final 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; } 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. * * @param i The intent * @return The response code as integer. */ private int getResponseCodeFromIntent(@NonNull final 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; } 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()); } } // JAVADOC:OFF private void flagStartAsync(final 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); } private void flagEndAsync() { logDebug("Ending async operation: " + mAsyncOperation); mAsyncOperation = ""; mAsyncInProgress = false; } private int queryPurchases(@NonNull final Inventory inv, final String itemType) throws JSONException, RemoteException { // Query purchases logDebug("Querying owned items, item type: " + itemType); logDebug("Package name: " + mContext.getPackageName()); boolean verificationFailed = false; String continueToken = null; do { logDebug("Calling getPurchases with continuation token: " + continueToken); Bundle ownedItems = mService.getPurchases(API_VERSION, mContext.getPackageName(), itemType, 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); if (ownedSkus == null) { ownedSkus = new ArrayList<>(); } ArrayList<String> purchaseDataList = ownedItems.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST); if (purchaseDataList == null) { purchaseDataList = new ArrayList<>(); } ArrayList<String> signatureList = ownedItems.getStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST); if (signatureList == null) { signatureList = new ArrayList<>(); } 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(itemType, 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; } private int querySkuDetails(final String itemType, @NonNull final Inventory inv, @Nullable final List<String> moreSkus) throws RemoteException, JSONException { logDebug("Querying SKU details."); ArrayList<String> skuList = new ArrayList<>(); skuList.addAll(inv.getAllOwnedSkus(itemType)); if (moreSkus != null) { for (String sku : moreSkus) { if (!skuList.contains(sku)) { skuList.add(sku); } } } 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(API_VERSION, mContext.getPackageName(), itemType, querySkus); if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) { int response = getResponseCodeFromBundle(skuDetails); if (response == BILLING_RESPONSE_RESULT_OK) { logError("getSkuDetails() returned a bundle with neither an error nor a detail list."); return IABHELPER_BAD_RESPONSE; } else { logDebug("getSkuDetails() failed: " + getResponseDesc(response)); return response; } } ArrayList<String> responseList = skuDetails.getStringArrayList(RESPONSE_GET_SKU_DETAILS_LIST); if (responseList == null) { responseList = new ArrayList<>(); } for (String thisResponse : responseList) { SkuDetails d = new SkuDetails(itemType, thisResponse); logDebug("Got sku details: " + d); inv.addSkuDetails(d); } return BILLING_RESPONSE_RESULT_OK; } private void consumeAsyncInternal(@NonNull final List<Purchase> purchases, @Nullable final OnConsumeFinishedListener singleListener, @Nullable final OnConsumeMultiFinishedListener multiListener) { final Handler handler = new Handler(); flagStartAsync(OPERATION_CONSUME); (new Thread(new Runnable() { @Override public void run() { final List<IabResult> results = new ArrayList<>(); 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 (!mDisposed && singleListener != null) { handler.post(new Runnable() { @Override public void run() { singleListener.onConsumeFinished(purchases.get(0), results.get(0)); } }); } if (!mDisposed && multiListener != null) { handler.post(new Runnable() { @Override public void run() { multiListener.onConsumeMultiFinished(purchases, results); } }); } } })).start(); } private void logDebug(final String msg) { if (mDebugLog) { Log.d(mDebugTag, msg); } } // JAVADOC:ON private void logError(final String msg) { Log.e(mDebugTag, "In-app billing error: " + msg); } private void logWarn(final String msg) { Log.w(mDebugTag, "In-app billing warning: " + msg); } /** * 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. */ 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. */ void onConsumeMultiFinished(List<Purchase> purchases, List<IabResult> results); } /** * 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) */ void onIabPurchaseFinished(IabResult result, Purchase info); } /** * 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. */ void onIabSetupFinished(IabResult result); } /** * 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. */ void onQueryInventoryFinished(IabResult result, Inventory inv); } }