package org.mtransit.android.util.iab; 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.content.pm.ResolveInfo; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.text.TextUtils; import com.android.vending.billing.IInAppBillingService; import org.json.JSONException; import org.mtransit.android.commons.MTLog; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; // based on the Google IAB sample (Apache License, Version 2.0) public class IabHelper implements MTLog.Loggable { private static final String TAG = IabHelper.class.getSimpleName(); @Override public String getLogTag() { return TAG; } boolean mSetupDone = false; boolean mDisposed = false; boolean mSubscriptionsSupported = false; boolean mAsyncInProgress = false; String mAsyncOperation = ""; Context mContext; IInAppBillingService mService; ServiceConnection mServiceConn; int mRequestCode; String mPurchasingItemType; String mSignatureBase64 = null; public static final int BILLING_RESPONSE_RESULT_OK = 0; public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1; public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3; public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4; public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5; public static final int BILLING_RESPONSE_RESULT_ERROR = 6; public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7; public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8; public static final int IABHELPER_ERROR_BASE = -1000; public static final int IABHELPER_REMOTE_EXCEPTION = -1001; public static final int IABHELPER_BAD_RESPONSE = -1002; public static final int IABHELPER_VERIFICATION_FAILED = -1003; public static final int IABHELPER_SEND_INTENT_FAILED = -1004; public static final int IABHELPER_USER_CANCELLED = -1005; public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006; public static final int IABHELPER_MISSING_TOKEN = -1007; public static final int IABHELPER_UNKNOWN_ERROR = -1008; public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009; public static final int IABHELPER_INVALID_CONSUMPTION = -1010; public static final String RESPONSE_CODE = "RESPONSE_CODE"; public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST"; public static final String RESPONSE_BUY_INTENT = "BUY_INTENT"; public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA"; public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE"; public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST"; public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST"; public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST"; public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN"; public static final String ITEM_TYPE_INAPP = "inapp"; public static final String ITEM_TYPE_SUBS = "subs"; public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST"; public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST"; public IabHelper(Context ctx, String base64PublicKey) { mContext = ctx.getApplicationContext(); mSignatureBase64 = base64PublicKey; } public interface OnIabSetupFinishedListener { void onIabSetupFinished(IabResult result); } public void startSetup(final OnIabSetupFinishedListener listener) { checkNotDisposed(); if (mSetupDone) throw new IllegalStateException("IAB helper is already set up."); mServiceConn = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { mService = null; } @Override public void onServiceConnected(ComponentName name, IBinder service) { if (mDisposed) return; mService = IInAppBillingService.Stub.asInterface(service); String packageName = mContext.getPackageName(); try { int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP); if (response != BILLING_RESPONSE_RESULT_OK) { if (listener != null) listener.onIabSetupFinished(new IabResult(response, "Error checking for billing v3 support.")); mSubscriptionsSupported = false; return; } response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS); if (response == BILLING_RESPONSE_RESULT_OK) { mSubscriptionsSupported = true; } mSetupDone = true; } catch (RemoteException e) { if (listener != null) { listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION, "RemoteException while setting up in-app billing.")); } MTLog.e(IabHelper.this, e, ">ServiceConnection> RemoteException while setting up in-app billing."); 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"); List<ResolveInfo> queryIntentServices = mContext == null ? null : mContext.getPackageManager().queryIntentServices(serviceIntent, 0); if (queryIntentServices != null && !queryIntentServices.isEmpty()) { mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE); } else { if (listener != null) { listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, "Billing service unavailable on device.")); } } } public void dispose() { mSetupDone = false; if (mServiceConn != null) { try { if (mContext != null && mService != null) mContext.unbindService(mServiceConn); } catch (Exception e) { // fix crash in production MTLog.w(this, e, "Error while unbinding service!"); } } mDisposed = true; mContext = null; mServiceConn = null; mService = null; mPurchaseListener = null; } private void checkNotDisposed() { if (mDisposed) throw new IllegalStateException("IabHelper was disposed of, so it cannot be used."); } public boolean subscriptionsSupported() { checkNotDisposed(); return mSubscriptionsSupported; } public interface OnIabPurchaseFinishedListener { void onIabPurchaseFinished(IabResult result, Purchase info); } OnIabPurchaseFinishedListener mPurchaseListener; public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) { launchPurchaseFlow(act, sku, requestCode, listener, ""); } public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener, String extraData) { launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData); } public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) { launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, ""); } public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener, String extraData) { launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData); } public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode, OnIabPurchaseFinishedListener listener, String extraData) { checkNotDisposed(); checkSetupDone("launchPurchaseFlow"); flagStartAsync("launchPurchaseFlow"); IabResult result; if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) { IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE, "Subscriptions are not available."); flagEndAsync(); if (listener != null) listener.onIabPurchaseFinished(r, null); return; } try { Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData); int response = getResponseCodeFromBundle(buyIntentBundle); if (response != BILLING_RESPONSE_RESULT_OK) { MTLog.e(this, "In-app billing error: Unable to buy item, Error response: %s", getResponseDesc(response)); flagEndAsync(); result = new IabResult(response, "Unable to buy item"); if (listener != null) listener.onIabPurchaseFinished(result, null); return; } PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); mRequestCode = requestCode; mPurchaseListener = listener; mPurchasingItemType = itemType; act.startIntentSenderForResult(pendingIntent == null ? null : pendingIntent.getIntentSender(), requestCode, new Intent(), 0, 0, 0); } catch (SendIntentException e) { MTLog.e(this, e, "In-app billing error: SendIntentException while launching purchase flow for sku " + sku); flagEndAsync(); result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent."); if (listener != null) listener.onIabPurchaseFinished(result, null); } catch (RemoteException e) { MTLog.e(this, e, "In-app billing error: RemoteException while launching purchase flow for sku " + sku); flagEndAsync(); result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow"); if (listener != null) listener.onIabPurchaseFinished(result, null); } } public boolean handleActivityResult(int requestCode, int resultCode, Intent data) { IabResult result; if (requestCode != mRequestCode) return false; checkNotDisposed(); checkSetupDone("handleActivityResult"); flagEndAsync(); if (data == null) { MTLog.e(this, "In-app billing error: 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) { if (purchaseData == null || dataSignature == null) { MTLog.e(this, "In-app billing error: BUG: either purchaseData or dataSignature is null."); 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(); if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) { MTLog.e(this, "In-app billing error: Purchase signature verification FAILED for sku %s", sku); result = new IabResult(IABHELPER_VERIFICATION_FAILED, String.format("Signature verification failed for sku %s", sku)); if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase); return true; } } catch (JSONException e) { MTLog.e(this, e, "In-app billing error: Failed to parse purchase data."); 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) { if (mPurchaseListener != null) { result = new IabResult(responseCode, "Problem purchashing item."); mPurchaseListener.onIabPurchaseFinished(result, null); } } else if (resultCode == Activity.RESULT_CANCELED) { result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled."); if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); } else { MTLog.e(this, "In-app billing error: Purchase failed. Result code: %s. Response: %s", Integer.toString(resultCode), getResponseDesc(responseCode)); result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response."); if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); } return true; } public Inventory queryInventory(boolean querySkuDetails, List<String> moreItemSkus, 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 (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, moreSubsSkus); 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); } } public interface QueryInventoryFinishedListener { void onQueryInventoryFinished(IabResult result, Inventory inv); } public void queryInventoryAsync(final boolean querySkuDetails, final List<String> moreItemSkus, final List<String> moreSubsSkus, QueryInventoryFinishedListener listenerHR) { final Handler handler = new Handler(); final WeakReference<QueryInventoryFinishedListener> listenerWR = listenerHR == null ? null : new WeakReference<QueryInventoryFinishedListener>( listenerHR); 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, moreItemSkus, moreSubsSkus); } catch (IabException ex) { result = ex.getResult(); } flagEndAsync(); final IabResult result_f = result; final Inventory inv_f = inv; if (!mDisposed) { handler.post(new Runnable() { @Override public void run() { QueryInventoryFinishedListener listener = listenerWR == null ? null : listenerWR.get(); if (listener != null) { listener.onQueryInventoryFinished(result_f, inv_f); } } }); } } })).start(); } public void queryInventoryAsync(QueryInventoryFinishedListener listener) { queryInventoryAsync(true, null, null, listener); } public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) { queryInventoryAsync(querySkuDetails, null, null, listener); } void consume(Purchase itemInfo) throws IabException { checkNotDisposed(); checkSetupDone("consume"); if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) { throw new IabException(IABHELPER_INVALID_CONSUMPTION, String.format("Items of type '%s' can't be consumed.", itemInfo.mItemType)); } try { String token = itemInfo.getToken(); String sku = itemInfo.getSku(); if (token == null || token.equals("")) { MTLog.e(this, "In-app billing error: Can't consume %s. No token.", sku); throw new IabException(IABHELPER_MISSING_TOKEN, String.format("PurchaseInfo is missing token for sku: %s %s", sku, itemInfo)); } int response = mService.consumePurchase(3, mContext.getPackageName(), token); if (response != BILLING_RESPONSE_RESULT_OK) { throw new IabException(response, String.format("Error consuming sku %s", sku)); } } catch (RemoteException e) { throw new IabException(IABHELPER_REMOTE_EXCEPTION, String.format("Remote exception while consuming. PurchaseInfo: %s", itemInfo), e); } } public interface OnConsumeFinishedListener { void onConsumeFinished(Purchase purchase, IabResult result); } public interface OnConsumeMultiFinishedListener { void onConsumeMultiFinished(List<Purchase> purchases, List<IabResult> results); } public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) { checkNotDisposed(); checkSetupDone("consume"); List<Purchase> purchases = new ArrayList<Purchase>(); purchases.add(purchase); consumeAsyncInternal(purchases, listener, null); } public void consumeAsync(List<Purchase> purchases, OnConsumeMultiFinishedListener listener) { checkNotDisposed(); checkSetupDone("consume"); consumeAsyncInternal(purchases, null, listener); } public static String getResponseDesc(int code) { String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" + "3:Billing Unavailable/4:Item unavailable/" + "5:Developer Error/6:Error/7:Item Already Owned/" + "8:Item not owned").split("/"); String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" + "-1002:Bad response received/" + "-1003:Purchase signature verification failed/" + "-1004:Send intent failed/" + "-1005:User cancelled/" + "-1006:Unknown purchase response/" + "-1007:Missing token/" + "-1008:Unknown error/" + "-1009:Subscriptions not available/" + "-1010:Invalid consumption attempt").split("/"); if (code <= IABHELPER_ERROR_BASE) { int index = IABHELPER_ERROR_BASE - code; if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index]; else return String.valueOf(code) + ":Unknown IAB Helper Error"; } else if (code < 0 || code >= iab_msgs.length) return String.valueOf(code) + ":Unknown"; else return iab_msgs[code]; } void checkSetupDone(String operation) { if (!mSetupDone) { MTLog.e(this, "In-app billing error: Illegal state for operation (%s): IAB helper is not set up.", operation); throw new IllegalStateException(String.format("IAB helper is not set up. Can't perform operation: %s", operation)); } } int getResponseCodeFromBundle(Bundle b) { Object o = b.get(RESPONSE_CODE); if (o == null) { return BILLING_RESPONSE_RESULT_OK; } else if (o instanceof Integer) return ((Integer) o); else if (o instanceof Long) return (int) ((Long) o).longValue(); else { MTLog.e(this, "In-app billing error: Unexpected type for bundle response code."); MTLog.e(this, "%s", o.getClass().getName()); throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName()); } } int getResponseCodeFromIntent(Intent i) { Object o = i.getExtras().get(RESPONSE_CODE); if (o == null) { MTLog.e(this, "In-app billing error: 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 { MTLog.e(this, "In-app billing error: Unexpected type for intent response code: %s", o.getClass().getName()); MTLog.e(this, "%s", o.getClass().getName()); throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName()); } } void flagStartAsync(String operation) { if (mAsyncInProgress) throw new IllegalStateException(String.format("Can't start async operation (%s) because another async operation (%s) is in progress.", operation, mAsyncOperation)); mAsyncOperation = operation; mAsyncInProgress = true; } void flagEndAsync() { mAsyncOperation = ""; mAsyncInProgress = false; } int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException { boolean verificationFailed = false; String continueToken = null; if (mContext == null || mService == null) { return IABHELPER_VERIFICATION_FAILED; } do { Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(), itemType, continueToken); int response = getResponseCodeFromBundle(ownedItems); if (response != BILLING_RESPONSE_RESULT_OK) { return response; } if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST) || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST) || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) { MTLog.e(this, "In-app billing error: Bundle returned from getPurchases() doesn't contain required fields."); return IABHELPER_BAD_RESPONSE; } ArrayList<String> purchaseDataList = ownedItems.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST); ArrayList<String> signatureList = ownedItems.getStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST); if (purchaseDataList != null) { for (int i = 0; i < purchaseDataList.size(); ++i) { String purchaseData = purchaseDataList.get(i); String signature = signatureList == null ? null : signatureList.get(i); if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) { Purchase purchase = new Purchase(itemType, purchaseData, signature); if (TextUtils.isEmpty(purchase.getToken())) { MTLog.w(this, "In-app billing warning: BUG: empty/null token!"); } inv.addPurchase(purchase); } else { MTLog.w(this, "In-app billing warning: Purchase signature verification **FAILED**. Not adding item."); verificationFailed = true; } } } continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN); } while (!TextUtils.isEmpty(continueToken)); return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK; } private static final int MAX_SKU_PER_REQUEST = 20; int querySkuDetails(String itemType, Inventory inv, List<String> moreSkus) throws RemoteException, JSONException { 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.size() == 0) { return BILLING_RESPONSE_RESULT_OK; } for (int start = 0; start < skuList.size(); start += MAX_SKU_PER_REQUEST) { int end = skuList.size() - start >= MAX_SKU_PER_REQUEST ? MAX_SKU_PER_REQUEST : skuList.size(); Bundle querySkus = new Bundle(); querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, new ArrayList<String>(skuList.subList(start, end))); Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(), itemType, querySkus); if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) { int response = getResponseCodeFromBundle(skuDetails); if (response != BILLING_RESPONSE_RESULT_OK) { return response; } else { MTLog.e(this, "In-app billing error: getSkuDetails() returned a bundle with neither an error nor a detail list."); return IABHELPER_BAD_RESPONSE; } } ArrayList<String> responseList = skuDetails.getStringArrayList(RESPONSE_GET_SKU_DETAILS_LIST); if (responseList != null) { for (String thisResponse : responseList) { SkuDetails d = new SkuDetails(itemType, thisResponse); inv.addSkuDetails(d); } } } return BILLING_RESPONSE_RESULT_OK; } void consumeAsyncInternal(final List<Purchase> purchases, final OnConsumeFinishedListener singleListener, final OnConsumeMultiFinishedListener multiListener) { final Handler handler = new Handler(); flagStartAsync("consume"); (new Thread(new Runnable() { @Override public void run() { final List<IabResult> results = new ArrayList<IabResult>(); for (Purchase purchase : purchases) { try { consume(purchase); results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku())); } catch (IabException ex) { results.add(ex.getResult()); } } flagEndAsync(); if (!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(); } }