/* 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.appsimobile.appsii.iab; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import com.android.vending.billing.IInAppBillingService; /** * A base class for iab-features. Provides connecting to the iab-service. * Subclasses can implement features like purchases or inventory checks */ public abstract class BaseIabHelper { // Billing response codes 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; // IAB Helper error codes 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 int IABHELPER_DEVELOPER_PAYLOAD_FAILED = -2001; // Keys for the responses from InAppBillingService 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"; // Item types public static final String ITEM_TYPE_INAPP = "inapp"; public static final String ITEM_TYPE_SUBS = "subs"; // some fields on the getSkuDetails response bundle 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"; static final String RSA_CODE = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmd6LF44ZpxvxO3j3" + "LTrwNqAL4HRgIdHhKwzLuF4rGiF4M/5ktv26gP/7toUonspr9LYDkk9xSi9JuyxVmpWvAyysCAVBnfr6CXx" + "DYB6aVophrMxiQkXDZ+onwFGc/T0igA0PLe1Hq4sA1Raa8NJeu08CzAJ/UuZLAHDqWVxt+myCMQJqTpm3Z9" + "wO10y+ftQuhnA3h2Hhi0aGH3OgYzkRv5VtAFZx0i/vacmjlIclvafazoGX77VwzUnH5Py4omG2GpsM0pItn" + "BLRAkLTDhDch/e7XgzoWUrJp3JXOJF7iKxMkknZa68Z4W63oaa/2I1WgLJHRV/cx0rNrMVgYSUy7wIDAQAB"; // Is debug logging enabled? static boolean sDebugLog = true; static String sDebugTag = "IabBaseHelper"; // Context we were passed during initialization final Context mContext; // Is setup done? boolean mSetupDone = false; // Are subscriptions supported? boolean mSubscriptionsSupported = false; // Connection to the service IInAppBillingService mService; ServiceConnection mServiceConn; // Public key for verifying signature, in base64 encoding String mSignatureBase64 = null; /** * 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 BaseIabHelper(Context ctx) { mContext = ctx.getApplicationContext(); mSignatureBase64 = RSA_CODE; logDebug("IAB helper created."); } static void logDebug(String msg) { if (sDebugLog) Log.d(sDebugTag, msg); } /** * 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 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]; } } // Workaround to bug where sometimes response codes come as Long instead of Integer static int getResponseCodeFromBundle(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()); } } static void logError(String msg) { Log.e(sDebugTag, "In-app billing error: " + msg); } // Workaround to bug where sometimes response codes come as Long instead of Integer static int getResponseCodeFromIntent(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()); } } static void logWarn(String msg) { Log.w(sDebugTag, "In-app billing warning: " + msg); } /** * Enables or disable debug logging through LogCat. */ public void enableDebugLogging(boolean enable, String tag) { sDebugLog = enable; sDebugTag = tag; } public void enableDebugLogging(boolean enable) { sDebugLog = 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 void startSetup(final OnIabSetupFinishedListener listener) { // If already set up, can't do it again. 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 onServiceConnected(ComponentName name, IBinder service) { 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(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.")); } // 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(3, 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.")); } } @Override public void onServiceDisconnected(ComponentName name) { logDebug("Billing service disconnected."); mService = null; } }; Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) { // service available to handle that Intent serviceIntent.setPackage("com.android.vending"); mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE); } else { // no service available to handle that Intent if (listener != null) { listener.onIabSetupFinished( new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, "Billing service unavailable on device.")); } } } /** * 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."); try { if (mContext != null) mContext.unbindService(mServiceConn); } catch (IllegalArgumentException ignore) { } mServiceConn = null; mService = null; } } // Checks that setup was done; if not, throws an exception. 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); } } /** * 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); } }