package com.jdroid.android.google.inappbilling; 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 com.jdroid.android.application.AbstractApplication; import com.jdroid.android.google.inappbilling.Product.ItemType; import com.jdroid.java.collections.Lists; import com.jdroid.java.collections.Maps; import com.jdroid.java.concurrent.ExecutorUtils; import com.jdroid.java.exception.ErrorCode; import com.jdroid.java.exception.ErrorCodeException; import com.jdroid.java.collections.CollectionUtils; import com.jdroid.java.utils.LoggerUtils; import org.json.JSONException; import org.slf4j.Logger; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * 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. * * After instantiating, you must perform setup in order to start using the object. To perform setup, call the * {@link #startSetup} method and provide a listener; that listener will be notified when setup is complete, after which * (and not before) you may call other methods. * * After setup is complete, you will typically want to request an inventory of owned items and subscriptions. See * {@link #queryInventory}, {@link #queryInventory(List, List)} and related methods. * * When you are done with this object, don't forget to call {@link #dispose} to ensure proper cleanup. This object holds * a binding to the in-app billing service, which will leak unless you dispose of it correctly. If you created the * object on an Activity's onCreate method, then the recommended place to dispose of it is the Activity's onDestroy * method. * * A note about threading: When using this object from a background thread, you may call the blocking versions of * methods; when using from a UI thread, call only the asynchronous versions and handle the results via callbacks. Also, * notice that you can only call one asynchronous operation at a time; attempting to start a second asynchronous * operation while the first one has not yet completed will result in an exception being thrown. * */ public class InAppBillingClient { private final static Logger LOGGER = LoggerUtils.getLogger(InAppBillingClient.class); private static final int IN_APP_BILLING_API_VERSION = 3; // 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"; public static final int PURCHASE_REQUEST_CODE = 10001; // some fields on the getSkuDetails response bundle private static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST"; // Has this object been disposed of? (If so, we should ignore callbacks, etc) private boolean disposed = false; // Are subscriptions supported? private boolean subscriptionsSupported = false; // Is an asynchronous operation in progress? // (only one at a time can be in progress) private boolean asyncInProgress = false; private String asyncOperation = ""; // Context we were passed during initialization private Context context; // Connection to the service private IInAppBillingService service; private ServiceConnection serviceConnection; // The request code used to launch purchase flow private int requestCode; // Public key for verifying signature, in base64 encoding private String signatureBase64 = null; // The product id used to launch the purchase flow private String productId; private InAppBillingClientListener listener; private Inventory inventory; /** * 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. */ public InAppBillingClient(Context ctx) { context = ctx.getApplicationContext(); signatureBase64 = InAppBillingAppModule.get().getInAppBillingContext().getGooglePlayPublicKey(); LOGGER.debug("InAppBillingClient 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. */ public void startSetup() { try { LOGGER.debug("Starting in-app billing setup."); serviceConnection = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { LOGGER.debug("Billing service disconnected."); service = null; } @Override public void onServiceConnected(ComponentName name, IBinder binder) { if (disposed) { return; } LOGGER.debug("Billing service connected."); service = IInAppBillingService.Stub.asInterface(binder); String packageName = context.getPackageName(); try { LOGGER.debug("Checking in-app billing " + IN_APP_BILLING_API_VERSION + " support for item type " + ItemType.MANAGED); int response = service.isBillingSupported(IN_APP_BILLING_API_VERSION, packageName, ItemType.MANAGED.getType()); ErrorCode inAppBillingErrorCode = InAppBillingErrorCode.findByErrorResponseCode(response); if (inAppBillingErrorCode == null) { LOGGER.debug("In-app billing supported for item type " + ItemType.MANAGED); LOGGER.debug("Checking in-app billing " + IN_APP_BILLING_API_VERSION + " support for item type " + ItemType.SUBSCRIPTION); response = service.isBillingSupported(IN_APP_BILLING_API_VERSION, packageName, ItemType.SUBSCRIPTION.getType()); inAppBillingErrorCode = InAppBillingErrorCode.findByErrorResponseCode(response); if (inAppBillingErrorCode == null) { LOGGER.debug("In-app billing supported for item type " + ItemType.SUBSCRIPTION); subscriptionsSupported = true; } else { LOGGER.warn("Subscriptions NOT AVAILABLE. InAppBillingErrorCode: " + inAppBillingErrorCode); } LOGGER.debug("In-app billing setup successful."); if (listener != null) { listener.onSetupFinished(); } } else { if (listener != null) { listener.onSetupFailed(inAppBillingErrorCode.newErrorCodeException()); } // if in-app purchases aren't supported, neither are subscriptions. subscriptionsSupported = false; } } catch (RemoteException e) { if (listener != null) { listener.onSetupFailed(InAppBillingErrorCode.REMOTE_EXCEPTION.newErrorCodeException(e)); } } catch (Exception e) { if (listener != null) { listener.onSetupFailed(InAppBillingErrorCode.UNEXPECTED_ERROR.newErrorCodeException(e)); } } } }; Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); serviceIntent.setPackage("com.android.vending"); List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentServices(serviceIntent, 0); if (CollectionUtils.isNotEmpty(resolveInfos)) { // service available to handle that Intent context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE); } else { // no service available to handle that Intent if (listener != null) { listener.onSetupFailed(InAppBillingErrorCode.BILLING_UNAVAILABLE.newErrorCodeException()); } } } catch (Exception e) { if (listener != null) { listener.onSetupFailed(InAppBillingErrorCode.UNEXPECTED_ERROR.newErrorCodeException(e)); } } } /** * This will query all supported items from the server. This will do so asynchronously and call back the specified * listener upon completion. This method is safe to call from a UI thread. * * @param managedProductTypes the managed {@link ProductType}s supported by the app * @param subscriptionsProductTypes the subscriptions {@link ProductType}s supported by the app */ public void queryInventory(final List<ProductType> managedProductTypes, final List<ProductType> subscriptionsProductTypes) { final Handler handler = new Handler(); if (flagStartAsync("queryInventory")) { ExecutorUtils.execute(new Runnable() { @Override public void run() { try { ErrorCodeException errorCodeException = null; Inventory inventory = null; try { inventory = queryInventoryInner(managedProductTypes, subscriptionsProductTypes); InAppBillingAppModule.get().getInAppBillingContext().setPurchasedProductTypes(inventory); } catch (ErrorCodeException e) { errorCodeException = e; } flagEndAsync(); final ErrorCodeException errorCodeExceptionFinal = errorCodeException; final Inventory inventoryFinal = inventory; if (!disposed) { handler.post(new Runnable() { @Override public void run() { if (listener != null) { if (errorCodeExceptionFinal == null) { listener.onQueryInventoryFinished(inventoryFinal); } else { listener.onQueryInventoryFailed(errorCodeExceptionFinal); } } } }); } } catch (Exception e) { if (listener != null) { listener.onQueryInventoryFailed(InAppBillingErrorCode.UNEXPECTED_ERROR.newErrorCodeException(e)); } } } }); } } /** * Queries the inventory. This will query all owned items from the server, as well as information on additional skus * This method may block or take long to execute. Do not call from a UI thread. * * @param managedProductTypes the managed {@link ProductType}s supported by the app * @param subscriptionsProductTypes the subscriptions {@link ProductType}s supported by the app * @return The {@link Inventory} * @throws ErrorCodeException if a problem occurs while refreshing the inventory. */ private Inventory queryInventoryInner(List<ProductType> managedProductTypes, List<ProductType> subscriptionsProductTypes) throws ErrorCodeException { inventory = null; if (!disposed) { inventory = new Inventory(); queryProductsDetails(inventory, ItemType.MANAGED, managedProductTypes); queryPurchases(inventory, ItemType.MANAGED); // if subscriptions are supported, then also query for subscriptions if (subscriptionsSupported) { queryProductsDetails(inventory, ItemType.SUBSCRIPTION, subscriptionsProductTypes); queryPurchases(inventory, ItemType.SUBSCRIPTION); } LOGGER.debug("Query inventory was successful."); } else { LOGGER.warn("Client disposed. Not queried inventary"); } return inventory; } private void queryPurchases(Inventory inventory, ItemType itemType) throws ErrorCodeException { LOGGER.debug("Querying owned items, item type: " + itemType); String continueToken = null; try { do { LOGGER.debug("Calling getPurchases with continuation token: " + continueToken); Bundle ownedItems = service.getPurchases(IN_APP_BILLING_API_VERSION, context.getPackageName(), itemType.getType(), continueToken); InAppBillingErrorCode inAppBillingErrorCode = getResponseCode(ownedItems); if (inAppBillingErrorCode != null) { throw inAppBillingErrorCode.newErrorCodeException("getPurchases() failed querying " + itemType); } List<String> ownedProductIds = ownedItems.getStringArrayList(RESPONSE_INAPP_ITEM_LIST); if (ownedProductIds == null) { throw InAppBillingErrorCode.BAD_RESPONSE.newErrorCodeException("Missing purchase item list from getPurchases()"); } List<String> purchaseDataList = ownedItems.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST); if (purchaseDataList == null) { throw InAppBillingErrorCode.MISSING_PURCHASE_DATA.newErrorCodeException("Missing purchase data list from getPurchases()"); } List<String> signatureList = ownedItems.getStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST); if (signatureList == null) { throw InAppBillingErrorCode.MISSING_DATA_SIGNATURE.newErrorCodeException("Missing data signature list from getPurchases()"); } for (int i = 0; i < purchaseDataList.size(); ++i) { String purchaseData = purchaseDataList.get(i); String signature = signatureList.get(i); String productId = ownedProductIds.get(i); Product product = inventory.getProduct(productId); if (product != null) { try { LOGGER.debug("Setting purchase to product: " + productId + ". " + purchaseData); product.setPurchase(signatureBase64, purchaseData, signature); } catch (ErrorCodeException e) { AbstractApplication.get().getExceptionHandler().logHandledException(e); } } else { AbstractApplication.get().getExceptionHandler().logWarningException( "The purchased product [" + productId + "] is not supported any more by the app, so it is ignored"); } } continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN); LOGGER.debug("Continuation token: " + continueToken); } while (!TextUtils.isEmpty(continueToken)); } catch (RemoteException e) { throw InAppBillingErrorCode.REMOTE_EXCEPTION.newErrorCodeException(e); } catch (JSONException e) { throw InAppBillingErrorCode.BAD_PURCHASE_DATA.newErrorCodeException(e); } } private void queryProductsDetails(Inventory inventory, ItemType itemType, List<ProductType> productTypes) throws ErrorCodeException { LOGGER.debug("Querying products details."); ArrayList<String> productsIdsToQuery = Lists.newArrayList(); for (ProductType each : productTypes) { if (!productsIdsToQuery.contains(each.getProductId())) { productsIdsToQuery.add(each.getProductId()); } } try { if (!productsIdsToQuery.isEmpty()) { Bundle bundle = new Bundle(); bundle.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, productsIdsToQuery); Bundle productsDetailsBundle = service.getSkuDetails(IN_APP_BILLING_API_VERSION, context.getPackageName(), itemType.getType(), bundle); List<String> skuDetailsList = productsDetailsBundle.getStringArrayList(RESPONSE_GET_SKU_DETAILS_LIST); Map<String, SkuDetails> map = Maps.newHashMap(); if (skuDetailsList != null) { for (String each : skuDetailsList) { SkuDetails skuDetails = new SkuDetails(each); map.put(skuDetails.getSku(), skuDetails); } for (ProductType each : productTypes) { SkuDetails skuDetails = map.get(each.getProductId()); if (skuDetails != null) { InAppBillingContext inAppBillingContext = InAppBillingAppModule.get().getInAppBillingContext(); ProductType productType = inAppBillingContext.isInAppBillingMockEnabled() ? inAppBillingContext.getTestProductType() : each; String title = each.getTitleId() != null ? context.getString(each.getTitleId()) : null; String description = each.getDescriptionId() != null ? context.getString(each.getDescriptionId()) : null; Product product = new Product(productType, skuDetails.getFormattedPrice(), skuDetails.getPrice(), skuDetails.getCurrencyCode(), title, description); LOGGER.debug("Adding to inventory: " + product); inventory.addProduct(product); } } } else { InAppBillingErrorCode inAppBillingErrorCode = getResponseCode(productsDetailsBundle); if (inAppBillingErrorCode != null) { throw inAppBillingErrorCode.newErrorCodeException("Failed querying " + itemType); } else { throw InAppBillingErrorCode.BAD_RESPONSE.newErrorCodeException("getSkuDetails() returned a bundle with neither an error nor a detail list."); } } } } catch (RemoteException e) { throw InAppBillingErrorCode.REMOTE_EXCEPTION.newErrorCodeException(e); } catch (JSONException e) { throw InAppBillingErrorCode.BAD_RESPONSE.newErrorCodeException(e); } } public void launchInAppPurchaseFlow(Activity activity, String productId, String devloperPayload) { launchPurchaseFlow(activity, productId, ItemType.MANAGED, PURCHASE_REQUEST_CODE, devloperPayload); } public void launchSubscriptionPurchaseFlow(Activity activity, String productId, int requestCode, String devloperPayload) { launchPurchaseFlow(activity, productId, ItemType.SUBSCRIPTION, PURCHASE_REQUEST_CODE, devloperPayload); } /** * 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 productId The product id 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 devloperPayload The 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(Activity activity, String productId, ItemType itemType, int requestCode, String devloperPayload) { if (flagStartAsync("launchPurchaseFlow")) { if (itemType.equals(ItemType.SUBSCRIPTION) && !subscriptionsSupported) { flagEndAsync(); if (listener != null) { listener.onPurchaseFailed(InAppBillingErrorCode.SUBSCRIPTIONS_NOT_AVAILABLE.newErrorCodeException()); } return; } try { LOGGER.debug("Constructing buy intent for product id " + productId + ", item type: " + itemType); Bundle buyIntentBundle = service.getBuyIntent(IN_APP_BILLING_API_VERSION, context.getPackageName(), productId, itemType.getType(), devloperPayload); InAppBillingErrorCode inAppBillingErrorCode = getResponseCode(buyIntentBundle); if (inAppBillingErrorCode == null) { PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); LOGGER.debug("Launching buy intent for product id " + productId + ". Request code: " + requestCode); this.requestCode = requestCode; this.productId = productId; activity.startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, new Intent(), 0, 0, 0); } else { flagEndAsync(); if (listener != null) { listener.onPurchaseFailed(inAppBillingErrorCode.newErrorCodeException()); } } } catch (SendIntentException e) { flagEndAsync(); if (listener != null) { listener.onPurchaseFailed(InAppBillingErrorCode.SEND_INTENT_FAILED.newErrorCodeException(e)); } } catch (RemoteException e) { flagEndAsync(); if (listener != null) { listener.onPurchaseFailed(InAppBillingErrorCode.REMOTE_EXCEPTION.newErrorCodeException(e)); } } } } /** * 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 onActivityResult method. This * method MUST be called from the UI thread of the Activity. * * @param requestCode The requestCode as you received it. * @param resultCode The resultCode as you received it. * @param data The data (Intent) as you received it. * @return Returns true if the result was related to a purchase flow and was handled; false if the result was not * related to a purchase, in which case you should handle it normally. */ public boolean handleActivityResult(int requestCode, int resultCode, Intent data) { if (this.requestCode != requestCode) { return false; } // end of async purchase operation that started on launchPurchaseFlow flagEndAsync(); if (data == null) { if (listener != null) { listener.onPurchaseFailed(InAppBillingErrorCode.BAD_RESPONSE.newErrorCodeException("Null data on activity result.")); } return true; } InAppBillingErrorCode inAppBillingErrorCode = getResponseCode(data.getExtras()); if ((resultCode == Activity.RESULT_OK) && (inAppBillingErrorCode == null)) { String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA); if (purchaseData == null) { if (listener != null) { listener.onPurchaseFailed(InAppBillingErrorCode.MISSING_PURCHASE_DATA.newErrorCodeException("PurchaseData is null. Extras: " + data.getExtras().toString())); } return true; } String signature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE); if (signature == null) { if (listener != null) { listener.onPurchaseFailed(InAppBillingErrorCode.MISSING_DATA_SIGNATURE.newErrorCodeException("DataSignature is null. Extras: " + data.getExtras().toString())); } return true; } LOGGER.debug("Successful resultcode from purchase activity."); LOGGER.debug("Purchase data: " + purchaseData); LOGGER.debug("Data signature: " + signature); LOGGER.debug("Extras: " + data.getExtras()); try { Product product = inventory.getProduct(productId); try { product.setPurchase(signatureBase64, purchaseData, signature); LOGGER.debug("Purchase signature successfully verified."); InAppBillingAppModule.get().getInAppBillingContext().addPurchasedProductType(product.getProductType()); InAppBillingAppModule.get().getAnalyticsSender().trackInAppBillingPurchase(product); if (listener != null) { listener.onPurchaseFinished(product); } } catch (ErrorCodeException e) { if (listener != null) { listener.onPurchaseFailed(e); } } } catch (JSONException e) { if (listener != null) { listener.onPurchaseFailed(InAppBillingErrorCode.BAD_PURCHASE_DATA.newErrorCodeException(e)); } } } else if (resultCode == Activity.RESULT_OK) { if (listener != null) { listener.onPurchaseFailed(inAppBillingErrorCode.newErrorCodeException("Result code was OK but in-app billing response was not OK")); } } else if (resultCode == Activity.RESULT_CANCELED) { if (listener != null) { if (inAppBillingErrorCode != null) { listener.onPurchaseFailed(inAppBillingErrorCode.newErrorCodeException("Purchase Failed. Result code: " + Integer.toString(resultCode))); } else { listener.onPurchaseFailed(InAppBillingErrorCode.USER_CANCELED.newErrorCodeException("Purchase canceled. Result code: " + Integer.toString(resultCode))); } } } else { if (listener != null) { if (inAppBillingErrorCode != null) { listener.onPurchaseFailed(inAppBillingErrorCode.newErrorCodeException("Purchase Failed. Result code: " + Integer.toString(resultCode))); } else { listener.onPurchaseFailed(InAppBillingErrorCode.UNKNOWN_PURCHASE_RESPONSE.newErrorCodeException("Purchase failed. Result code: " + Integer.toString(resultCode))); } } } return true; } /** * 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 product The {@link Product} to be consumed. */ public void consume(final Product product) { final Handler handler = new Handler(); if (flagStartAsync("consume")) { ExecutorUtils.execute(new Runnable() { @Override public void run() { try { ErrorCodeException errorCodeException = null; try { consumeInner(product); } catch (ErrorCodeException e) { errorCodeException = e; } flagEndAsync(); final ErrorCodeException errorCodeExceptionFinal = errorCodeException; if (!disposed) { handler.post(new Runnable() { @Override public void run() { if (listener != null) { if (errorCodeExceptionFinal == null) { listener.onConsumeFinished(product); } else { listener.onConsumeFailed(errorCodeExceptionFinal); } } } }); } } catch (Exception e) { if (listener != null) { listener.onQueryInventoryFailed(InAppBillingErrorCode.UNEXPECTED_ERROR.newErrorCodeException(e)); } } } }); } } /** * 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. * * @param product The {@link Product} that represents the item to consume. */ private void consumeInner(Product product) throws ErrorCodeException { if (product.getProductType().getItemType().equals(ItemType.MANAGED)) { try { String token = product.getPurchase().getToken(); String productId = product.getId(); if ((token == null) || token.equals("")) { throw InAppBillingErrorCode.MISSING_TOKEN.newErrorCodeException("Can't consume " + productId + ". No token."); } if (!disposed) { LOGGER.debug("Consuming productId: " + productId + ", token: " + token); int response = service.consumePurchase(IN_APP_BILLING_API_VERSION, context.getPackageName(), token); InAppBillingErrorCode inAppBillingErrorCode = InAppBillingErrorCode.findByErrorResponseCode(response); if (inAppBillingErrorCode == null) { product.consume(); LOGGER.debug("Successfully consumed productId: " + productId); } else { throw inAppBillingErrorCode.newErrorCodeException("Error consuming consuming productId " + productId); } } else { LOGGER.warn("Client disposed. Not consuming productId: " + productId + ", token: " + token); } } catch (RemoteException e) { throw InAppBillingErrorCode.REMOTE_EXCEPTION.newErrorCodeException(); } } else { throw InAppBillingErrorCode.INVALID_CONSUMPTION.newErrorCodeException("Items of type '" + product.getProductType().getItemType() + "' can't be consumed."); } } /** * 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() { LOGGER.debug("Disposing."); if (serviceConnection != null) { LOGGER.debug("Unbinding from service."); if (context != null) { try { context.unbindService(serviceConnection); } catch (RuntimeException e) { LOGGER.warn("Error on unbinding", e); } } } disposed = true; context = null; serviceConnection = null; service = null; listener = null; } private Boolean flagStartAsync(String operation) { if (asyncInProgress) { AbstractApplication.get().getExceptionHandler().logWarningException( "Can't start async operation (consumeAsyncInternal) because another (" + asyncOperation + ") is in progress."); return false; } asyncOperation = operation; asyncInProgress = true; LOGGER.debug("Starting async operation: " + operation); return true; } private void flagEndAsync() { LOGGER.debug("Ending async operation: " + asyncOperation); asyncOperation = null; asyncInProgress = false; } // Workaround to bug where sometimes response codes come as Long instead of Integer private InAppBillingErrorCode getResponseCode(Bundle bundle) { Object responseCode = bundle.get(RESPONSE_CODE); if (responseCode == null) { LOGGER.debug("Bundle/Intent with null response code, assuming OK (known issue)"); return null; } else if (responseCode instanceof Integer) { return InAppBillingErrorCode.findByErrorResponseCode(((Integer)responseCode)); } else if (responseCode instanceof Long) { return InAppBillingErrorCode.findByErrorResponseCode((int)((Long)responseCode).longValue()); } else { throw InAppBillingErrorCode.UNEXPECTED_ERROR.newErrorCodeException("Unexpected type for bundle response code: " + responseCode.getClass().getName()); } } /** * @return whether subscriptions are supported. */ public boolean subscriptionsSupported() { return subscriptionsSupported; } public void setInAppBillingClientListener(InAppBillingClientListener listener) { this.listener = listener; } }