diff --git a/library/src/org/onepf/oms/OpenIabHelper.java b/library/src/org/onepf/oms/OpenIabHelper.java index e14b5643..d3431cbc 100644 --- a/library/src/org/onepf/oms/OpenIabHelper.java +++ b/library/src/org/onepf/oms/OpenIabHelper.java @@ -1 +1,1330 @@ -/******************************************************************************* * Copyright 2013 One Platform Foundation * * 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 org.onepf.oms; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.onepf.oms.appstore.AmazonAppstore; import org.onepf.oms.appstore.FortumoStore; import org.onepf.oms.appstore.GooglePlay; import org.onepf.oms.appstore.NokiaStore; import org.onepf.oms.appstore.OpenAppstore; import org.onepf.oms.appstore.SamsungApps; import org.onepf.oms.appstore.SamsungAppsBillingService; import org.onepf.oms.appstore.TStore; import org.onepf.oms.appstore.googleUtils.IabException; import org.onepf.oms.appstore.googleUtils.IabHelper; import org.onepf.oms.appstore.googleUtils.IabHelper.OnIabPurchaseFinishedListener; import org.onepf.oms.appstore.googleUtils.IabHelper.OnIabSetupFinishedListener; import org.onepf.oms.appstore.googleUtils.IabHelper.QueryInventoryFinishedListener; import org.onepf.oms.appstore.googleUtils.IabResult; import org.onepf.oms.appstore.googleUtils.Inventory; import org.onepf.oms.appstore.googleUtils.Purchase; import org.onepf.oms.appstore.googleUtils.Security; import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.util.Log; /** * * * @author Boris Minaev, Oleg Orlov * @since 16.04.13 */ public class OpenIabHelper { private static String TAG = OpenIabHelper.class.getSimpleName(); //Is debug enabled? private static boolean isDebugLog = false; private static final String BIND_INTENT = "org.onepf.oms.openappstore.BIND"; /** */ private static final int DISCOVER_TIMEOUT_MS = 5000; /** * for generic stores it takes 1.5 - 3sec *

* SamsungApps initialization is very time consuming (from 4 to 12 seconds). * TODO: Optimize: ~1sec is consumed for check account certification via account activity + ~3sec for actual setup */ private static final int INVENTORY_CHECK_TIMEOUT_MS = 10000; /** Used for all communication with Android services */ private final Context context; /** Necessary to initialize SamsungApps. For other stuff {@link #context} is used */ private Activity activity; private Handler notifyHandler = null; /** selected appstore */ private Appstore mAppstore; /** selected appstore billing service */ private AppstoreInAppBillingService mAppstoreBillingService; private final Options options; private static final int SETUP_RESULT_NOT_STARTED = -1; private static final int SETUP_RESULT_SUCCESSFUL = 0; private static final int SETUP_RESULT_FAILED = 1; private static final int SETUP_DISPOSED = 2; private int setupState = SETUP_RESULT_NOT_STARTED; /** SamsungApps requires {@link #handleActivityResult(int, int, Intent)} but it doesn't * work until setup is completed. */ private volatile SamsungApps samsungInSetup; /** used to track time used for {@link #startSetup(OnIabSetupFinishedListener)} * TODO: think about smarter time tracker (i.e. Logger built-in) */ private volatile static long started; // 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 = ""; // The request code used to launch purchase flow int mRequestCode; // The item type of the current purchase flow String mPurchasingItemType; // Item types public static final String ITEM_TYPE_INAPP = "inapp"; public static final String ITEM_TYPE_SUBS = "subs"; // Billing response codes public static final int BILLING_RESPONSE_RESULT_OK = 0; public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3; public static final int BILLING_RESPONSE_RESULT_ERROR = 6; public static final String NAME_GOOGLE = "com.google.play"; public static final String NAME_AMAZON = "com.amazon.apps"; public static final String NAME_TSTORE = "com.tmobile.store"; public static final String NAME_SAMSUNG = "com.samsung.apps"; public static final String NAME_FORTUMO = "com.fortumo.billing"; public static final String NAME_YANDEX = "com.yandex.store"; public static final String NAME_NOKIA = "com.nokia.nstore"; /** * NOTE: used as sync object in related methods
* * storeName -> [ ... {app_sku1 -> store_sku1}, ... ] */ private static final Map > sku2storeSkuMappings = new HashMap>(); /** * storeName -> [ ... {store_sku1 -> app_sku1}, ... ] */ private static final Map > storeSku2skuMappings = new HashMap>(); /** * Map sku and storeSku for particular store. *

* The best approach is to use SKU that unique in universe like com.companyname.application.item. * Such SKU fit most of stores so it doesn't need to be mapped. *

* If best approach is not applicable use application inner SKU in code (usually it is SKU for Google Play) * and map SKU from other stores using this method. OpenIAB will map SKU in both directions, * so you can use only your inner SKU * * @param sku - application inner SKU * @param storeSku - shouldn't duplicate already mapped values * @param storeName - @see {@link IOpenAppstore#getAppstoreName()} or {@link #NAME_AMAZON} {@link #NAME_GOOGLE} {@link #NAME_TSTORE} */ public static void mapSku(String sku, String storeName, String storeSku) { synchronized (sku2storeSkuMappings) { Map skuMap = sku2storeSkuMappings.get(storeName); if (skuMap == null) { skuMap = new HashMap(); sku2storeSkuMappings.put(storeName, skuMap); } if (skuMap.get(sku) != null) { throw new IllegalArgumentException("Already specified SKU. sku: " + sku + " -> storeSku: " + skuMap.get(sku)); } Map storeSkuMap = storeSku2skuMappings.get(storeName); if (storeSkuMap == null) { storeSkuMap = new HashMap(); storeSku2skuMappings.put(storeName, storeSkuMap); } if (storeSkuMap.get(storeSku) != null) { throw new IllegalArgumentException("Ambigous SKU mapping. You try to map sku: " + sku + " -> storeSku: " + storeSku + ", that is already mapped to sku: " + storeSkuMap.get(storeSku)); } skuMap.put(sku, storeSku); storeSkuMap.put(storeSku, sku); } } /** * Return previously mapped store SKU for specified inner SKU * @see #mapSku(String, String, String) * * @param appstoreName * @param sku - inner SKU * @return SKU used in store for specified inner SKU */ public static String getStoreSku(final String appstoreName, String sku) { synchronized (sku2storeSkuMappings) { String currentStoreSku = sku; Map skuMap = sku2storeSkuMappings.get(appstoreName); if (skuMap != null && skuMap.get(sku) != null) { currentStoreSku = skuMap.get(sku); if (isDebugLog()) Log.d(TAG, "getStoreSku() using mapping for sku: " + sku + " -> " + currentStoreSku); } return currentStoreSku; } } /** * Return mapped application inner SKU using store name and store SKU. * @see #mapSku(String, String, String) */ public static String getSku(final String appstoreName, String storeSku) { synchronized (sku2storeSkuMappings) { String sku = storeSku; Map skuMap = storeSku2skuMappings.get(appstoreName); if (skuMap != null && skuMap.get(sku) != null) { sku = skuMap.get(sku); if (isDebugLog()) Log.d(TAG, "getSku() restore sku from storeSku: " + storeSku + " -> " + sku); } return sku; } } /** * @param appstoreName for example {@link OpenIabHelper#NAME_AMAZON} * @return list of skus those have mappings for specified appstore */ public static List getAllStoreSkus(final String appstoreName) { Map skuMap = sku2storeSkuMappings.get(appstoreName); List result = new ArrayList(); if (skuMap != null) { result.addAll(skuMap.values()); } return result; } /** * Simple constructor for OpenIabHelper. *

See {@link OpenIabHelper#OpenIabHelper(Context, Options)} for details * * @param storeKeys - see {@link Options#storeKeys} * @param context - if you want to support Samsung Apps you must pass an Activity, in other cases any context is acceptable */ public OpenIabHelper(Context context, Map storeKeys) { this(context, storeKeys, null); } /** * Simple constructor for OpenIabHelper. *

See {@link OpenIabHelper#OpenIabHelper(Context, Options)} for details * * @param storeKeys - see {@link Options#storeKeys} * @param prefferedStores - see {@link Options#prefferedStoreNames} * @param context - if you want to support Samsung Apps you must pass an Activity, in other cases any context is acceptable */ public OpenIabHelper(Context context, Map storeKeys, String[] prefferedStores) { this(context, storeKeys, prefferedStores, null); } /** * Simple constructor for OpenIabHelper. *

See {@link OpenIabHelper#OpenIabHelper(Context, Options)} for details * * @param storeKeys - see {@link Options#storeKeys} * @param prefferedStores - see {@link Options#prefferedStoreNames} * @param availableStores - see {@link Options#availableStores} * @param context - if you want to support Samsung Apps you must pass an Activity, in other cases any context is acceptable */ public OpenIabHelper(Context context, Map storeKeys, String[] prefferedStores, Appstore[] availableStores) { this.context = context.getApplicationContext(); this.options = new Options(); if (context instanceof Activity) { this.activity = (Activity) context; } options.storeKeys = storeKeys; options.prefferedStoreNames = prefferedStores != null ? prefferedStores : options.prefferedStoreNames; options.availableStores = availableStores != null ? new ArrayList(Arrays.asList(availableStores)) : null; checkSettings(options, context); } /** * Before start ensure you already have

  • * - permission org.onepf.openiab.permission.BILLING in your AndroidManifest.xml
  • * - publicKey for store you decided to work with (you can find it in Developer Console of your store)
  • * - map SKUs for your store if they differs using {@link #mapSku(String, String, String)}
  • * *

    * You can specify publicKeys for stores (excluding Amazon and SamsungApps those don't use * verification based on RSA keys). See {@link Options#storeKeys} for details *

    * By default verification will be performed for receipt from every store. To aviod verification * exception OpenIAB doesn't connect to store that key is not specified for *

    * If you don't want to put publicKey in code and verify receipt remotely, you need to set * {@link Options#verifyMode} to {@link Options#VERIFY_SKIP}. * To make OpenIAB connect even to stores key is not specified for, use {@link Options#VERIFY_ONLY_KNOWN} *

    * {@link Options#prefferedStoreNames} is useful option when you test your app on device with multiple * stores installed. Specify store name you want to work with here and it would be selected if you * install application using adb. * * @param options - specify all necessary options * @param context - if you want to support Samsung Apps you must pass an Activity, in other cases any context is acceptable */ public OpenIabHelper(Context context, Options options) { this.context = context.getApplicationContext(); this.options = options; if (context instanceof Activity) { this.activity = (Activity) context; } checkSettings(options, context); } /** * Discover all available stores and select the best billing service. * If the flag {@link org.onepf.oms.OpenIabHelper.Options#checkInventory} is set to true, stores with existing inventory are checked first. If Fortumo is added as an * available store or the flag {@link org.onepf.oms.OpenIabHelper.Options#supportFortumo} is set to true, it also will be checked for an inventory. * * Should be called from UI thread * @param listener - called when setup is completed */ public void startSetup(final IabHelper.OnIabSetupFinishedListener listener) { if (listener == null){ throw new IllegalArgumentException("Setup listener must be not null!"); } if (setupState != SETUP_RESULT_NOT_STARTED) { String state = setupStateToString(setupState); throw new IllegalStateException("Couldn't be set up. Current state: " + state); } this.notifyHandler = new Handler(); started = System.currentTimeMillis(); new Thread(new Runnable() { public void run() { List stores2check = new ArrayList(); if (options.availableStores != null) { stores2check.addAll(options.availableStores); } else { // if appstores are not specified by user - lookup for all available stores final List openStores = discoverOpenStores(context, null, options); if (isDebugLog()) Log.d(TAG, in() + " " + "startSetup() discovered openstores: " + openStores.toString()); stores2check.addAll(openStores); if (options.verifyMode == Options.VERIFY_EVERYTHING && !options.storeKeys.containsKey(NAME_GOOGLE)) { // don't work with GooglePlay if verifyMode is strict and no publicKey provided } else { final String publicKey = options.verifyMode == Options.VERIFY_SKIP ? null : options.storeKeys.get(OpenIabHelper.NAME_GOOGLE); stores2check.add(new GooglePlay(context, publicKey)); } // try AmazonApps if in-app-purchasing.jar with Amazon SDK is compiled with app try { OpenIabHelper.class.getClassLoader().loadClass("com.amazon.inapp.purchasing.PurchasingManager"); stores2check.add(new AmazonAppstore(context)); } catch (ClassNotFoundException e) {} // try T-Store if iap_plugin-dev.jar with T-Store SDK is compiled with app try { TStore.class.getClassLoader().loadClass("com.skplanet.dodo.IapPlugin"); stores2check.add(new TStore(context, options.storeKeys.get(OpenIabHelper.NAME_TSTORE))); } catch (ClassNotFoundException e) {} if (getAllStoreSkus(NAME_SAMSUNG).size() > 0) { // SamsungApps shows lot of UI stuff during init // try it only if samsung SKUs are specified stores2check.add(new SamsungApps(activity, options)); } stores2check.add(new NokiaStore(context)); } //todo redo boolean hasFortumoInSetup; if (BuildConfig.FORTUMO_ENABLE){ hasFortumoInSetup = false; for (Appstore store : stores2check) { if (store instanceof SamsungApps) { samsungInSetup = (SamsungApps) store; } else if (store instanceof FortumoStore) { hasFortumoInSetup = true; } } } IabResult result = new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, "Billing isn't supported"); if (options.checkInventory) { final List equippedStores = checkInventory(stores2check); if (equippedStores.size() > 0) { mAppstore = selectBillingService(equippedStores); } if (BuildConfig.FORTUMO_ENABLE && mAppstore == null) { if (!hasFortumoInSetup && options.supportFortumo) { mAppstore = FortumoStore.initFortumoStore(context, true); } } if (isDebugLog()) Log.d(TAG, in() + " " + "select equipped"); if (mAppstore != null) { final String message = "Successfully initialized with existing inventory: " + mAppstore.getAppstoreName(); result = new IabResult(BILLING_RESPONSE_RESULT_OK, message); if (isDebugLog()) { Log.d(TAG, message); } } else { // found no equipped stores. Select store based on store parameters mAppstore = selectBillingService(stores2check); if (BuildConfig.FORTUMO_ENABLE && mAppstore == null) { if (!hasFortumoInSetup && options.supportFortumo) { mAppstore = FortumoStore.initFortumoStore(context, false); } } if (isDebugLog()) Log.d(TAG, in() + " " + "select non-equipped"); if (mAppstore != null) { final String message = "Successfully initialized with non-equipped store: " + mAppstore.getAppstoreName(); result = new IabResult(BILLING_RESPONSE_RESULT_OK, message); if (isDebugLog()) { Log.d(TAG, message); } } } if (mAppstore != null) { mAppstoreBillingService = mAppstore.getInAppBillingService(); } fireSetupFinished(listener, result); } else { // no inventory check. Select store based on store parameters mAppstore = selectBillingService(stores2check); if (BuildConfig.FORTUMO_ENABLE && null == mAppstore) { if (!hasFortumoInSetup && options.supportFortumo) { mAppstore = FortumoStore.initFortumoStore(context, false); } } if (mAppstore != null) { mAppstoreBillingService = mAppstore.getInAppBillingService(); mAppstoreBillingService.startSetup(new OnIabSetupFinishedListener() { public void onIabSetupFinished(IabResult result) { fireSetupFinished(listener, result); } }); } else { fireSetupFinished(listener, result); } } for (Appstore store : stores2check) { if (store != mAppstore && store.getInAppBillingService() != null) { store.getInAppBillingService().dispose(); if (isDebugLog()) Log.d(TAG, in() + " " + "startSetup() disposing " + store.getAppstoreName()); } } } }, "openiab-setup").start(); } /** * Must be called after setup is finished. See {@link #startSetup(OnIabSetupFinishedListener)} * @return null if no appstore connected, otherwise name of Appstore OpenIAB has connected to. */ public synchronized String getConnectedAppstoreName() { if (mAppstore == null) return null; return mAppstore.getAppstoreName(); } /** Check options are valid */ public static void checkOptions(Options options) { if (options.verifyMode != Options.VERIFY_SKIP && options.storeKeys != null) { // check publicKeys. Must be not null and valid for (Entry entry : options.storeKeys.entrySet()) { if (entry.getValue() == null) { throw new IllegalArgumentException("Null publicKey for store: " + entry.getKey() + ", key: " + entry.getValue()); } try { Security.generatePublicKey(entry.getValue()); } catch (Exception e) { throw new IllegalArgumentException("Invalid publicKey for store: " + entry.getKey() + ", key: " + entry.getValue(), e); } } } } private static void checkSettings(Options options, Context context){ checkOptions(options); checkSamsung(context); if (BuildConfig.FORTUMO_ENABLE) { checkFortumo(options, context); } } private static void checkFortumo(Options options, Context context) { if (BuildConfig.FORTUMO_ENABLE) { boolean checkFortumo = options.supportFortumo; if (!checkFortumo && options.availableStores != null) { for (Appstore store : options.availableStores) { if (store instanceof FortumoStore) { checkFortumo = true; break; } } } if (checkFortumo) { StringBuilder resultBuilder = new StringBuilder(); //is Fortumo lib available? StringBuilder jarResultBuilder = new StringBuilder(); try { FortumoStore.class.getClassLoader().loadClass("mp.MpUtils"); } catch (ClassNotFoundException e) { jarResultBuilder.append(" \n - Fortumo classes CAN'T be loaded."); } //manifest StringBuilder manifestResultBuilder = new StringBuilder(); checkPermission(context, "android.permission.INTERNET", manifestResultBuilder); checkPermission(context, "android.permission.ACCESS_NETWORK_STATE", manifestResultBuilder); checkPermission(context, "android.permission.READ_PHONE_STATE", manifestResultBuilder); // checkPermission(context, "android.permission.RECEIVE_SMS", manifestResultBuilder); // checkPermission(context, "android.permission.SEND_SMS", manifestResultBuilder); Intent paymentActivityIntent = new Intent(); paymentActivityIntent.setClassName(context.getPackageName(), "mp.MpActivity"); if (context.getPackageManager().resolveActivity(paymentActivityIntent, 0) == null) { formatComponentStatus(" - Required mp.MpActivity is NOT declared.", manifestResultBuilder); } Intent mpServerIntent = new Intent(); mpServerIntent.setClassName(context.getPackageName(), "mp.MpService"); if (context.getPackageManager().resolveService(mpServerIntent, 0) == null) { formatComponentStatus(" - Required mp.MpService is NOT declared.", manifestResultBuilder); } Intent statusUpdateServiceIntent = new Intent(); statusUpdateServiceIntent.setClassName(context.getPackageName(), "mp.StatusUpdateService"); if (context.getPackageManager().resolveService(statusUpdateServiceIntent, 0) == null) { formatComponentStatus(" - Required mp.StatusUpdateService is NOT declared.", manifestResultBuilder); } //xml StringBuilder xmlStringBuilder = new StringBuilder(); try { final List strings = Arrays.asList(context.getResources().getAssets().list("")); final boolean hasProductFile = strings.contains(FortumoStore.IN_APP_PRODUCTS_FILE_NAME); final boolean hasFortumoDetailsFile = strings.contains(FortumoStore.FORTUMO_DETAILS_FILE_NAME); if (!hasProductFile) { xmlStringBuilder.append(" - Required file " + FortumoStore.IN_APP_PRODUCTS_FILE_NAME + " NOT found in /assets."); } if (!hasFortumoDetailsFile) { if (!hasProductFile) { xmlStringBuilder.append('\n'); } xmlStringBuilder.append(" - Required file " + FortumoStore.FORTUMO_DETAILS_FILE_NAME + " NOT found in /assets."); } } catch (IOException e) { if (xmlStringBuilder.length() > 0) { xmlStringBuilder.append('\n'); } xmlStringBuilder.append("- Xml files CANNOT be parsed."); } final boolean noJar = jarResultBuilder.length() > 0; final boolean smthWrongWithManifest = manifestResultBuilder.length() > 0; final boolean smthWrongWithgXmlFiles = xmlStringBuilder.length() > 0; if (noJar || smthWrongWithManifest || smthWrongWithgXmlFiles) { resultBuilder.append("\nFortumo setup failed for the following reasons:"); if (noJar) { resultBuilder.append('\n'); resultBuilder.append(jarResultBuilder); } if (smthWrongWithgXmlFiles) { resultBuilder.append('\n'); resultBuilder.append(xmlStringBuilder); } if(smthWrongWithManifest){ resultBuilder.append('\n'); resultBuilder.append(manifestResultBuilder); } } if (resultBuilder.length() > 0) { resultBuilder.append('\n') .append("********************************************************************************************************\n") .append("* To support Fortumo follow the instructions of https://github.com/onepf/OpenIAB/blob/master/README.md *\n") .append("********************************************************************************************************"); throw new IllegalStateException(resultBuilder.toString(), null); } } } } //todo move to Utils private static void checkPermission(Context context, String paramString, StringBuilder builder) { if (context.checkCallingOrSelfPermission(paramString) != PackageManager.PERMISSION_GRANTED) { if (builder.length() > 0) { builder.append('\n'); } builder.append(String.format(" - Required permission \"%s\" is NOT granted.", paramString)); } } //todo move to Utils private static void formatComponentStatus(String message, StringBuilder messageBuilder){ if (messageBuilder.length() > 0) { messageBuilder.append('\n'); } messageBuilder.append(message); } private static void checkSamsung(Context context) { List allStoreSkus = getAllStoreSkus(OpenIabHelper.NAME_SAMSUNG); if (!allStoreSkus.isEmpty()) { // it means that Samsung is among the candidates for (String sku : allStoreSkus) { SamsungApps.checkSku(sku); } if (!(context instanceof Activity)) { // // Unfortunately, SamsungApps requires to launch their own "Certification Activity" // in order to connect to billing service. So it's also needed for OpenIAB. // // Because of SKU for SamsungApps are specified, // intance of Activity needs to be passed to OpenIAB constructor to launch // Samsung Cerfitication Activity. // Activity also need to pass activityResult to OpenIABHelper.handleActivityResult() // // throw new IllegalArgumentException( "\n " + "\nContext is not instance of Activity." + "\nUnfortunately, SamsungApps requires to launch their own Certification Activity " + "\nin order to connect to billing service. So it's also needed for OpenIAB." + "\n " + "\nBecause of SKU for SamsungApps are specified, instance of Activity needs to be passed " + "\nto OpenIAB constructor to launch Samsung Cerfitication Activity." + "\nActivity should call OpenIabHelper#handleActivityResult()." + "\n "); } } } protected void fireSetupFinished(final IabHelper.OnIabSetupFinishedListener listener, final IabResult result) { if (setupState == SETUP_DISPOSED) return; if (isDebugLog()) Log.d(TAG, in() + " " + "fireSetupFinished() === SETUP DONE === result: " + result + (mAppstore != null ? ", appstore: " + mAppstore.getAppstoreName() : "")); samsungInSetup = null; setupState = result.isSuccess() ? SETUP_RESULT_SUCCESSFUL : SETUP_RESULT_FAILED; notifyHandler.post(new Runnable() { public void run() { listener.onIabSetupFinished(result); } }); } /** * Discover all OpenStore services, checks them and build {@link #availableStores} list
    *

    * Lock current thread for {@link Options#discoveryTimeoutMs}
    * Must not be called from main thread to avoid service connection blocking * * @param dest - discovered OpenStores will be added here. If null new List() will be created * @param options - settings for Appstore discovery like verifyMode and timeouts * * @return dest or new List with discovered Appstores */ public static List discoverOpenStores(final Context context, final List dest, final Options options) { if (Thread.currentThread().equals(Looper.getMainLooper().getThread())) { throw new IllegalStateException("Must not be called from main thread. " + "Service interaction will be blocked"); } PackageManager packageManager = context.getPackageManager(); final Intent intentAppstoreServices = new Intent(BIND_INTENT); List infoList = packageManager.queryIntentServices(intentAppstoreServices, 0); final List result = dest != null ? dest : new ArrayList(infoList != null ? infoList.size() : 0); if (infoList == null || infoList.isEmpty()) { return result; } final CountDownLatch storesToCheck = new CountDownLatch(infoList.size()); for (ResolveInfo info : infoList) { String packageName = info.serviceInfo.packageName; String name = info.serviceInfo.name; Intent intentAppstore = new Intent(intentAppstoreServices); intentAppstore.setClassName(packageName, name); try { boolean isBound = context.bindService(intentAppstore, new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { if (isDebugLog()) Log.d(TAG, "discoverOpenStores() appstoresService connected for component: " + name.flattenToShortString()); IOpenAppstore openAppstoreService = IOpenAppstore.Stub.asInterface(service); try { String appstoreName = openAppstoreService.getAppstoreName(); Intent billingIntent = openAppstoreService.getBillingServiceIntent(); if (appstoreName == null) { // no name - no service Log.e(TAG, "discoverOpenStores() Appstore doesn't have name. Skipped. ComponentName: " + name); } else if (billingIntent == null) { // don't handle stores without billing support if (isDebugLog()) Log.d(TAG, "discoverOpenStores(): billing is not supported by store: " + name); } else if ((options.verifyMode == Options.VERIFY_EVERYTHING) && !options.storeKeys.containsKey(appstoreName)) { // don't connect to OpenStore if no key provided and verification is strict Log.e(TAG, "discoverOpenStores() verification is required but publicKey is not provided: " + name); } else { String publicKey = options.storeKeys.get(appstoreName); if (options.verifyMode == Options.VERIFY_SKIP) publicKey = null; final OpenAppstore openAppstore = new OpenAppstore(context, appstoreName, openAppstoreService, billingIntent, publicKey, this); openAppstore.componentName = name; Log.d(TAG, "discoverOpenStores() add new OpenStore: " + openAppstore); synchronized (result) { if (result.contains(openAppstore) == false) { result.add(openAppstore); } } } } catch (RemoteException e) { Log.e(TAG, "discoverOpenStores() ComponentName: " + name, e); } storesToCheck.countDown(); } @Override public void onServiceDisconnected(ComponentName name) { if (isDebugLog()) Log.d(TAG, "onServiceDisconnected() appstoresService disconnected for component: " + name.flattenToShortString()); //Nothing to do here } }, Context.BIND_AUTO_CREATE); if (!isBound) { storesToCheck.countDown(); } }catch (SecurityException e){ Log.e(TAG, "bindService() failed for " + packageName, e); storesToCheck.countDown(); } } try { storesToCheck.await(options.discoveryTimeoutMs, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Log.e(TAG, "Interrupted: discovering OpenStores. ", e); } return result; } /** * Connects to Billing Service of each store. Request list of user purchases (inventory) * * @see {@link OpenIabHelper#INVENTORY_CHECK_TIMEOUT_MS} to set timout value * * @param availableStores - list of stores to check * @return list of stores with non-empty inventory */ protected List checkInventory(final List availableStores) { String packageName = context.getPackageName(); // candidates: Map candidates = new HashMap(); for (Appstore appstore : availableStores) { if (appstore.isBillingAvailable(packageName)) { candidates.put(appstore.getAppstoreName(), appstore); } } if (isDebugLog()) Log.d(TAG, in() + " " + candidates.size() + " inventory candidates"); final List equippedStores = Collections.synchronizedList(new ArrayList()); final CountDownLatch storeRemains = new CountDownLatch(candidates.size()); // for every appstore: connect to billing service and check inventory for (Map.Entry entry : candidates.entrySet()) { final Appstore appstore = entry.getValue(); final AppstoreInAppBillingService billingService = entry.getValue().getInAppBillingService(); billingService.startSetup(new OnIabSetupFinishedListener() { public void onIabSetupFinished(IabResult result) { if (isDebugLog()) Log.d(TAG, in() + " " + "billing set " + appstore.getAppstoreName()); if(result.isFailure()) { storeRemains.countDown(); return; } new Thread(new Runnable() { public void run() { try { Inventory inventory = billingService.queryInventory(false, null, null); if (inventory.getAllPurchases().size() > 0) { equippedStores.add(appstore); } if (isDebugLog()) Log.d(TAG, in() + " " + "inventoryCheck() in " + appstore.getAppstoreName() + " found: " + inventory.getAllPurchases().size() + " purchases"); } catch (IabException e) { Log.e(TAG, "inventoryCheck() failed for " + appstore.getAppstoreName()); } storeRemains.countDown(); } }, "inv-check[" + appstore.getAppstoreName()+ "]").start(); } }); } try { storeRemains.await(options.checkInventoryTimeoutMs, TimeUnit.MILLISECONDS); if (isDebugLog()) Log.d(TAG, in() + " " + "inventory check done"); } catch (InterruptedException e) { Log.e(TAG, "selectBillingService() inventory check is failed. candidates: " + candidates.size() + ", inventory remains: " + storeRemains.getCount() , e); } return equippedStores; } /** * Lookup for requested service in store based on isPackageInstaller() & isBillingAvailable() *

    * Scenario: *

  • * - look for installer: if exists and supports billing service - we done
  • * - rest of stores who support billing considered as candidates

  • * * - find candidate according to [prefferedStoreNames]. if found - we done

  • * * - select candidate randomly from 3 groups based on published package version
  • * - published version == app.versionCode
  • * - published version > app.versionCode
  • * - published version < app.versionCode * */ protected Appstore selectBillingService(final List availableStores) { String packageName = context.getPackageName(); // candidates: Map candidates = new HashMap(); // for (Appstore appstore : availableStores) { if (appstore.isBillingAvailable(packageName)) { candidates.put(appstore.getAppstoreName(), appstore); } else { continue; // for billing we cannot select store without billing } if (appstore.isPackageInstaller(packageName)) { return appstore; } } if (candidates.size() == 0) return null; // lookup for developer preffered stores for (int i = 0; i < options.prefferedStoreNames.length; i++) { Appstore candidate = candidates.get(options.prefferedStoreNames[i]); if (candidate != null) { return candidate; } } // nothing found. select something that matches package version int versionCode = Appstore.PACKAGE_VERSION_UNDEFINED; try { versionCode = context.getPackageManager().getPackageInfo(packageName, 0).versionCode; } catch (NameNotFoundException e) { Log.e(TAG, "Are we installed?", e); } List sameVersion = new ArrayList(); List higherVersion = new ArrayList(); for (Appstore candidate : candidates.values()) { final int storeVersion = candidate.getPackageVersion(packageName); if (storeVersion == versionCode) { sameVersion.add(candidate); } else if (storeVersion > versionCode) { higherVersion.add(candidate); } } // use random if found stores with same version of package if (sameVersion.size() > 0) { return sameVersion.get(new Random().nextInt(sameVersion.size())); } else if (higherVersion.size() > 0) { // or one of higher version return higherVersion.get(new Random().nextInt(higherVersion.size())); } else { // ok, return no matter what return new ArrayList(candidates.values()).get(new Random().nextInt(candidates.size())); } } public void dispose() { logDebug("Disposing."); if (mAppstoreBillingService != null) { mAppstoreBillingService.dispose(); } setupState = SETUP_DISPOSED; } public boolean subscriptionsSupported() { checkSetupDone("subscriptionsSupported"); return mAppstoreBillingService.subscriptionsSupported(); } public void launchPurchaseFlow(Activity act, String sku, int requestCode, IabHelper.OnIabPurchaseFinishedListener listener) { launchPurchaseFlow(act, sku, requestCode, listener, ""); } public void launchPurchaseFlow(Activity act, String sku, int requestCode, IabHelper.OnIabPurchaseFinishedListener listener, String extraData) { launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData); } public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, IabHelper.OnIabPurchaseFinishedListener listener) { launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, ""); } public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, IabHelper.OnIabPurchaseFinishedListener listener, String extraData) { launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData); } public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode, IabHelper.OnIabPurchaseFinishedListener listener, String extraData) { checkSetupDone("launchPurchaseFlow"); String storeSku = getStoreSku(mAppstore.getAppstoreName(), sku); mAppstoreBillingService.launchPurchaseFlow(act, storeSku, itemType, requestCode, listener, extraData); } public boolean handleActivityResult(int requestCode, int resultCode, Intent data) { if (isDebugLog()) Log.d(TAG, in() + " " + "handleActivityResult() requestCode: " + requestCode+ " resultCode: " + resultCode+ " data: " + data); if (requestCode == options.samsungCertificationRequestCode && samsungInSetup != null) { return samsungInSetup.getInAppBillingService().handleActivityResult(requestCode, resultCode, data); } if (setupState != SETUP_RESULT_SUCCESSFUL) { if (isDebugLog()) Log.d(TAG, "handleActivityResult() setup is not done. requestCode: " + requestCode+ " resultCode: " + resultCode+ " data: " + data); return false; } return mAppstoreBillingService.handleActivityResult(requestCode, resultCode, data); } /** * See {@link #queryInventory(boolean, List, List)} for details */ public Inventory queryInventory(boolean querySkuDetails, List 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 {@link #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. * @throws IabException if a problem occurs while refreshing the inventory. */ public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus, List moreSubsSkus) throws IabException { checkSetupDone("queryInventory"); List moreItemStoreSkus = null; if (moreItemSkus != null) { moreItemStoreSkus = new ArrayList(); for (String sku : moreItemSkus) { String storeSku = getStoreSku(mAppstore.getAppstoreName(), sku); moreItemStoreSkus.add(storeSku); } } List moreSubsStoreSkus = null; if (moreSubsSkus != null) { moreSubsStoreSkus = new ArrayList(); for (String sku : moreSubsSkus) { String storeSku = getStoreSku(mAppstore.getAppstoreName(), sku); moreSubsStoreSkus.add(storeSku); } } return mAppstoreBillingService.queryInventory(querySkuDetails, moreItemStoreSkus, moreSubsStoreSkus); } /** * Queries the inventory. This will query all owned items from the server, as well as * information on additional skus, if specified. This method may block or take long to execute. * Do not call from a UI thread. For that, use the non-blocking version {@link #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. * @throws IabException if a problem occurs while refreshing the inventory. */ public void queryInventoryAsync(final boolean querySkuDetails, final List moreItemSkus, final List moreSubsSkus, final IabHelper.QueryInventoryFinishedListener listener) { checkSetupDone("queryInventory"); if (listener == null) { throw new IllegalArgumentException("Inventory listener must be not null"); } flagStartAsync("refresh inventory"); (new Thread(new Runnable() { 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 (setupState != SETUP_DISPOSED) { notifyHandler.post(new Runnable() { public void run() { listener.onQueryInventoryFinished(result_f, inv_f); } }); } } })).start(); } /** * For details see {@link #queryInventoryAsync(boolean, List, List, QueryInventoryFinishedListener)} */ public void queryInventoryAsync(final boolean querySkuDetails, final List moreSkus, final IabHelper.QueryInventoryFinishedListener listener) { checkSetupDone("queryInventoryAsync"); if (listener == null) { throw new IllegalArgumentException("Inventory listener must be not null!"); } queryInventoryAsync(querySkuDetails, moreSkus, null, listener); } /** * For details see {@link #queryInventoryAsync(boolean, List, List, QueryInventoryFinishedListener)} */ public void queryInventoryAsync(IabHelper.QueryInventoryFinishedListener listener) { checkSetupDone("queryInventoryAsync"); if (listener == null) { throw new IllegalArgumentException("Inventory listener must be not null!"); } queryInventoryAsync(true, null, listener); } /** * For details see {@link #queryInventoryAsync(boolean, List, List, QueryInventoryFinishedListener)} */ public void queryInventoryAsync(boolean querySkuDetails, IabHelper.QueryInventoryFinishedListener listener) { checkSetupDone("queryInventoryAsync"); if (listener == null) { throw new IllegalArgumentException("Inventory listener must be not null!"); } queryInventoryAsync(querySkuDetails, null, listener); } public void consume(Purchase itemInfo) throws IabException { checkSetupDone("consume"); Purchase purchaseStoreSku = (Purchase) itemInfo.clone(); // TODO: use Purchase.getStoreSku() purchaseStoreSku.setSku(getStoreSku(mAppstore.getAppstoreName(), itemInfo.getSku())); mAppstoreBillingService.consume(purchaseStoreSku); } public void consumeAsync(Purchase purchase, IabHelper.OnConsumeFinishedListener listener) { checkSetupDone("consumeAsync"); if (listener == null) { throw new IllegalArgumentException("Consume listener must be not null!"); } List purchases = new ArrayList(); purchases.add(purchase); consumeAsyncInternal(purchases, listener, null); } public void consumeAsync(List purchases, IabHelper.OnConsumeMultiFinishedListener listener) { checkSetupDone("consumeAsync"); if (listener == null) { throw new IllegalArgumentException("Consume listener must be not null!"); } consumeAsyncInternal(purchases, null, listener); } void consumeAsyncInternal(final List purchases, final IabHelper.OnConsumeFinishedListener singleListener, final IabHelper.OnConsumeMultiFinishedListener multiListener) { checkSetupDone("consume"); flagStartAsync("consume"); (new Thread(new Runnable() { public void run() { final List 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 (setupState != SETUP_DISPOSED && singleListener != null) { notifyHandler.post(new Runnable() { public void run() { singleListener.onConsumeFinished(purchases.get(0), results.get(0)); } }); } if (setupState != SETUP_DISPOSED && multiListener != null) { notifyHandler.post(new Runnable() { public void run() { multiListener.onConsumeMultiFinished(purchases, results); } }); } } })).start(); } // Checks that setup was done; if not, throws an exception. void checkSetupDone(String operation) { String stateToString = setupStateToString(setupState); if (setupState != SETUP_RESULT_SUCCESSFUL) { logError("Illegal state for operation (" + operation + "): " + stateToString); throw new IllegalStateException(stateToString + " Can't perform operation: " + operation); } } void flagStartAsync(String operation) { // TODO: why can't be called consume and queryInventory at the same time? // 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); } void flagEndAsync() { logDebug("Ending async operation: " + mAsyncOperation); mAsyncOperation = ""; mAsyncInProgress = false; } void logDebug(String msg) { if (isDebugLog()) Log.d(TAG, msg); } void logError(String msg) { Log.e(TAG, "In-app billing error: " + msg); } void logWarn(String msg) { if (isDebugLog()) Log.w(TAG, "In-app billing warning: " + msg); } private static String setupStateToString(int setupState) { String state; if (setupState == SETUP_RESULT_NOT_STARTED) { state = " IAB helper is not set up."; } else if (setupState == SETUP_DISPOSED) { state = "IAB helper was disposed of."; } else if (setupState == SETUP_RESULT_SUCCESSFUL) { state = "IAB helper is set up."; } else if (setupState == SETUP_RESULT_FAILED) { state = "IAB helper setup failed."; } else { throw new IllegalStateException("Wrong setup state: " + setupState); } return state; } public interface OnInitListener { public void onInitFinished(); } public interface OnOpenIabHelperInitFinished { public void onOpenIabHelperInitFinished(); } private static String in() { return "in: " + (System.currentTimeMillis() - started); } public static boolean isDebugLog() { return OpenIabHelper.isDebugLog || Log.isLoggable(TAG, Log.DEBUG); } public static void enableDebugLogging(boolean enabled) { OpenIabHelper.isDebugLog = enabled; } public static void enableDebugLogging(boolean enabled, String tag) { OpenIabHelper.isDebugLog = enabled; OpenIabHelper.TAG = tag; } public static boolean isPackageInstaller(Context appContext, String installer) { String installerPackageName = appContext.getPackageManager().getInstallerPackageName(appContext.getPackageName()); return installerPackageName != null && installerPackageName.equals(installer); } /** * All options of OpenIAB can be found here * * TODO: consider to use cloned instance of Options in OpenIABHelper */ public static class Options { /** * List of stores to be used for store elections. By default GooglePlay, Amazon, SamsungApps and * all installed OpenStores are used. *

    * To specify your own list, you need to instantiate Appstore object manually. * GooglePlay, Amazon and SamsungApps could be instantiated directly. OpenStore can be discovered * using {@link OpenIabHelper#discoverOpenStores(Context, List, Options)} *

    * If you put only your instance of Appstore in this list OpenIAB will use it * * TODO: consider to use AppstoreFactory.get(storeName) -> Appstore instance */ public List availableStores; /** * Wait specified amount of ms to find all OpenStores on device */ public int discoveryTimeoutMs = DISCOVER_TIMEOUT_MS; /** * Check user inventory in every store to select proper store *

    * Will try to connect to each billingService and extract user's purchases. * If purchases have been found in the only store that store will be used for further purchases. * If purchases have been found in multiple stores only such stores will be used for further elections */ public boolean checkInventory = true; /** * Wait specified amount of ms to check inventory in all stores */ public int checkInventoryTimeoutMs = INVENTORY_CHECK_TIMEOUT_MS; /** * OpenIAB could skip receipt verification by publicKey for GooglePlay and OpenStores *

    * Receipt could be verified in {@link OnIabPurchaseFinishedListener#onIabPurchaseFinished()} * using {@link Purchase#getOriginalJson()} and {@link Purchase#getSignature()} */ public int verifyMode = VERIFY_EVERYTHING; /** * Verify signatures in any store. *

    * By default in Google's IabHelper. Throws exception if key is not available or invalid. * To prevent crashes OpenIAB wouldn't connect to OpenStore if no publicKey provided */ public static final int VERIFY_EVERYTHING = 0; /** * Don't verify signatires. To perform verification on server-side */ public static final int VERIFY_SKIP = 1; /** * Verify signatures only if publicKey is available. Otherwise skip verification. *

    * Developer is responsible for verify */ public static final int VERIFY_ONLY_KNOWN = 2; /** * storeKeys is map of [ appstore name -> publicKeyBase64 ] * Put keys for all stores you support in this Map and pass it to instantiate {@link OpenIabHelper} *

    * publicKey key is used to verify receipt is created by genuine Appstore using * provided signature. It can be found in Developer Console of particular store *

    * name of particular store can be provided by local_store tool if you run it on device. * For Google Play OpenIAB uses {@link OpenIabHelper#NAME_GOOGLE}. *

    *

    Note: * AmazonApps and SamsungApps doesn't use RSA keys for receipt verification, so you don't need * to specify it */ public Map storeKeys = new HashMap(); /** * Used as priority list if store that installed app is not found and there are * multiple stores installed on device that supports billing. */ public String[] prefferedStoreNames = new String[] {}; /** Used for SamsungApps setup. Specify your own value if default one interfere your code. *

    default value is {@link SamsungAppsBillingService#REQUEST_CODE_IS_ACCOUNT_CERTIFICATION} */ public int samsungCertificationRequestCode = SamsungAppsBillingService.REQUEST_CODE_IS_ACCOUNT_CERTIFICATION; /** * Is Fortumo supported? * */ public boolean supportFortumo; } } \ No newline at end of file +/******************************************************************************* + * Copyright 2013 One Platform Foundation + * + * 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 org.onepf.oms; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.onepf.oms.appstore.AmazonAppstore; +import org.onepf.oms.appstore.FortumoStore; +import org.onepf.oms.appstore.GooglePlay; +import org.onepf.oms.appstore.NokiaStore; +import org.onepf.oms.appstore.OpenAppstore; +import org.onepf.oms.appstore.SamsungApps; +import org.onepf.oms.appstore.SamsungAppsBillingService; +import org.onepf.oms.appstore.TStore; +import org.onepf.oms.appstore.googleUtils.IabException; +import org.onepf.oms.appstore.googleUtils.IabHelper; +import org.onepf.oms.appstore.googleUtils.IabHelper.OnIabPurchaseFinishedListener; +import org.onepf.oms.appstore.googleUtils.IabHelper.OnIabSetupFinishedListener; +import org.onepf.oms.appstore.googleUtils.IabHelper.QueryInventoryFinishedListener; +import org.onepf.oms.appstore.googleUtils.IabResult; +import org.onepf.oms.appstore.googleUtils.Inventory; +import org.onepf.oms.appstore.googleUtils.Purchase; +import org.onepf.oms.appstore.googleUtils.Security; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; + +/** + * + * + * @author Boris Minaev, Oleg Orlov + * @since 16.04.13 + */ +public class OpenIabHelper { + private static String TAG = OpenIabHelper.class.getSimpleName(); + + //Is debug enabled? + private static boolean isDebugLog = false; + + private static final String BIND_INTENT = "org.onepf.oms.openappstore.BIND"; + + /** */ + private static final int DISCOVER_TIMEOUT_MS = 5000; + + /** + * for generic stores it takes 1.5 - 3sec + *

    + * SamsungApps initialization is very time consuming (from 4 to 12 seconds). + * TODO: Optimize: ~1sec is consumed for check account certification via account activity + ~3sec for actual setup + */ + private static final int INVENTORY_CHECK_TIMEOUT_MS = 10000; + + /** Used for all communication with Android services */ + private final Context context; + /** Necessary to initialize SamsungApps. For other stuff {@link #context} is used */ + private Activity activity; + + private Handler notifyHandler = null; + + /** selected appstore */ + private Appstore mAppstore; + + /** selected appstore billing service */ + private AppstoreInAppBillingService mAppstoreBillingService; + + private final Options options; + + private static final int SETUP_RESULT_NOT_STARTED = -1; + private static final int SETUP_RESULT_SUCCESSFUL = 0; + private static final int SETUP_RESULT_FAILED = 1; + private static final int SETUP_DISPOSED = 2; + private int setupState = SETUP_RESULT_NOT_STARTED; + + /** SamsungApps requires {@link #handleActivityResult(int, int, Intent)} but it doesn't + * work until setup is completed. */ + private volatile SamsungApps samsungInSetup; + + /** used to track time used for {@link #startSetup(OnIabSetupFinishedListener)} + * TODO: think about smarter time tracker (i.e. Logger built-in) */ + private volatile static long started; + + // 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 = ""; + + // The request code used to launch purchase flow + int mRequestCode; + + // The item type of the current purchase flow + String mPurchasingItemType; + + // Item types + public static final String ITEM_TYPE_INAPP = "inapp"; + public static final String ITEM_TYPE_SUBS = "subs"; + + // Billing response codes + public static final int BILLING_RESPONSE_RESULT_OK = 0; + public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3; + public static final int BILLING_RESPONSE_RESULT_ERROR = 6; + + public static final String NAME_GOOGLE = "com.google.play"; + public static final String NAME_AMAZON = "com.amazon.apps"; + public static final String NAME_TSTORE = "com.tmobile.store"; + public static final String NAME_SAMSUNG = "com.samsung.apps"; + public static final String NAME_FORTUMO = "com.fortumo.billing"; + public static final String NAME_YANDEX = "com.yandex.store"; + public static final String NAME_NOKIA = "com.nokia.nstore"; + + /** + * NOTE: used as sync object in related methods
    + * + * storeName -> [ ... {app_sku1 -> store_sku1}, ... ] + */ + private static final Map > sku2storeSkuMappings = new HashMap>(); + + /** + * storeName -> [ ... {store_sku1 -> app_sku1}, ... ] + */ + private static final Map > storeSku2skuMappings = new HashMap>(); + + /** + * Map sku and storeSku for particular store. + *

    + * The best approach is to use SKU that unique in universe like com.companyname.application.item. + * Such SKU fit most of stores so it doesn't need to be mapped. + *

    + * If best approach is not applicable use application inner SKU in code (usually it is SKU for Google Play) + * and map SKU from other stores using this method. OpenIAB will map SKU in both directions, + * so you can use only your inner SKU + * + * @param sku - application inner SKU + * @param storeSku - shouldn't duplicate already mapped values + * @param storeName - @see {@link IOpenAppstore#getAppstoreName()} or {@link #NAME_AMAZON} {@link #NAME_GOOGLE} {@link #NAME_TSTORE} + */ + public static void mapSku(String sku, String storeName, String storeSku) { + synchronized (sku2storeSkuMappings) { + Map skuMap = sku2storeSkuMappings.get(storeName); + if (skuMap == null) { + skuMap = new HashMap(); + sku2storeSkuMappings.put(storeName, skuMap); + } + if (skuMap.get(sku) != null) { + throw new IllegalArgumentException("Already specified SKU. sku: " + sku + " -> storeSku: " + skuMap.get(sku)); + } + Map storeSkuMap = storeSku2skuMappings.get(storeName); + if (storeSkuMap == null) { + storeSkuMap = new HashMap(); + storeSku2skuMappings.put(storeName, storeSkuMap); + } + if (storeSkuMap.get(storeSku) != null) { + throw new IllegalArgumentException("Ambigous SKU mapping. You try to map sku: " + sku + " -> storeSku: " + storeSku + ", that is already mapped to sku: " + storeSkuMap.get(storeSku)); + } + skuMap.put(sku, storeSku); + storeSkuMap.put(storeSku, sku); + } + } + + /** + * Return previously mapped store SKU for specified inner SKU + * @see #mapSku(String, String, String) + * + * @param appstoreName + * @param sku - inner SKU + * @return SKU used in store for specified inner SKU + */ + public static String getStoreSku(final String appstoreName, String sku) { + synchronized (sku2storeSkuMappings) { + String currentStoreSku = sku; + Map skuMap = sku2storeSkuMappings.get(appstoreName); + if (skuMap != null && skuMap.get(sku) != null) { + currentStoreSku = skuMap.get(sku); + if (isDebugLog()) Log.d(TAG, "getStoreSku() using mapping for sku: " + sku + " -> " + currentStoreSku); + } + return currentStoreSku; + } + } + + /** + * Return mapped application inner SKU using store name and store SKU. + * @see #mapSku(String, String, String) + */ + public static String getSku(final String appstoreName, String storeSku) { + synchronized (sku2storeSkuMappings) { + String sku = storeSku; + Map skuMap = storeSku2skuMappings.get(appstoreName); + if (skuMap != null && skuMap.get(sku) != null) { + sku = skuMap.get(sku); + if (isDebugLog()) Log.d(TAG, "getSku() restore sku from storeSku: " + storeSku + " -> " + sku); + } + return sku; + } + } + + /** + * @param appstoreName for example {@link OpenIabHelper#NAME_AMAZON} + * @return list of skus those have mappings for specified appstore + */ + public static List getAllStoreSkus(final String appstoreName) { + Map skuMap = sku2storeSkuMappings.get(appstoreName); + List result = new ArrayList(); + if (skuMap != null) { + result.addAll(skuMap.values()); + } + return result; + } + + /** + * Simple constructor for OpenIabHelper. + *

    See {@link OpenIabHelper#OpenIabHelper(Context, Options)} for details + * + * @param storeKeys - see {@link Options#storeKeys} + * @param context - if you want to support Samsung Apps you must pass an Activity, in other cases any context is acceptable + */ + public OpenIabHelper(Context context, Map storeKeys) { + this(context, storeKeys, null); + } + + /** + * Simple constructor for OpenIabHelper. + *

    See {@link OpenIabHelper#OpenIabHelper(Context, Options)} for details + * + * @param storeKeys - see {@link Options#storeKeys} + * @param prefferedStores - see {@link Options#prefferedStoreNames} + * @param context - if you want to support Samsung Apps you must pass an Activity, in other cases any context is acceptable + */ + public OpenIabHelper(Context context, Map storeKeys, String[] prefferedStores) { + this(context, storeKeys, prefferedStores, null); + } + + /** + * Simple constructor for OpenIabHelper. + *

    See {@link OpenIabHelper#OpenIabHelper(Context, Options)} for details + * + * @param storeKeys - see {@link Options#storeKeys} + * @param prefferedStores - see {@link Options#prefferedStoreNames} + * @param availableStores - see {@link Options#availableStores} + * @param context - if you want to support Samsung Apps you must pass an Activity, in other cases any context is acceptable + */ + public OpenIabHelper(Context context, Map storeKeys, String[] prefferedStores, Appstore[] availableStores) { + this.context = context.getApplicationContext(); + this.options = new Options(); + if (context instanceof Activity) { + this.activity = (Activity) context; + } + + options.storeKeys = storeKeys; + options.prefferedStoreNames = prefferedStores != null ? prefferedStores : options.prefferedStoreNames; + options.availableStores = availableStores != null ? new ArrayList(Arrays.asList(availableStores)) : null; + + checkSettings(options, context); + } + + /** + * Before start ensure you already have

  • + * - permission org.onepf.openiab.permission.BILLING in your AndroidManifest.xml
  • + * - publicKey for store you decided to work with (you can find it in Developer Console of your store)
  • + * - map SKUs for your store if they differs using {@link #mapSku(String, String, String)}
  • + * + *

    + * You can specify publicKeys for stores (excluding Amazon and SamsungApps those don't use + * verification based on RSA keys). See {@link Options#storeKeys} for details + *

    + * By default verification will be performed for receipt from every store. To aviod verification + * exception OpenIAB doesn't connect to store that key is not specified for + *

    + * If you don't want to put publicKey in code and verify receipt remotely, you need to set + * {@link Options#verifyMode} to {@link Options#VERIFY_SKIP}. + * To make OpenIAB connect even to stores key is not specified for, use {@link Options#VERIFY_ONLY_KNOWN} + *

    + * {@link Options#prefferedStoreNames} is useful option when you test your app on device with multiple + * stores installed. Specify store name you want to work with here and it would be selected if you + * install application using adb. + * + * @param options - specify all necessary options + * @param context - if you want to support Samsung Apps you must pass an Activity, in other cases any context is acceptable + */ + public OpenIabHelper(Context context, Options options) { + this.context = context.getApplicationContext(); + this.options = options; + if (context instanceof Activity) { + this.activity = (Activity) context; + } + + checkSettings(options, context); + } + + /** + * Discover all available stores and select the best billing service. + * If the flag {@link org.onepf.oms.OpenIabHelper.Options#checkInventory} is set to true, stores with existing inventory are checked first. If Fortumo is added as an + * available store or the flag {@link org.onepf.oms.OpenIabHelper.Options#supportFortumo} is set to true, it also will be checked for an inventory. + * + * Should be called from UI thread + * @param listener - called when setup is completed + */ + public void startSetup(final IabHelper.OnIabSetupFinishedListener listener) { + if (listener == null){ + throw new IllegalArgumentException("Setup listener must be not null!"); + } + if (setupState != SETUP_RESULT_NOT_STARTED) { + String state = setupStateToString(setupState); + throw new IllegalStateException("Couldn't be set up. Current state: " + state); + } + this.notifyHandler = new Handler(); + started = System.currentTimeMillis(); + new Thread(new Runnable() { + public void run() { + List stores2check = new ArrayList(); + if (options.availableStores != null) { + stores2check.addAll(options.availableStores); + } else { // if appstores are not specified by user - lookup for all available stores + final List openStores = discoverOpenStores(context, null, options); + if (isDebugLog()) Log.d(TAG, in() + " " + "startSetup() discovered openstores: " + openStores.toString()); + stores2check.addAll(openStores); + if (options.verifyMode == Options.VERIFY_EVERYTHING && !options.storeKeys.containsKey(NAME_GOOGLE)) { + // don't work with GooglePlay if verifyMode is strict and no publicKey provided + } else { + final String publicKey = options.verifyMode == Options.VERIFY_SKIP ? null + : options.storeKeys.get(OpenIabHelper.NAME_GOOGLE); + stores2check.add(new GooglePlay(context, publicKey)); + } + + // try AmazonApps if in-app-purchasing.jar with Amazon SDK is compiled with app + try { + OpenIabHelper.class.getClassLoader().loadClass("com.amazon.inapp.purchasing.PurchasingManager"); + stores2check.add(new AmazonAppstore(context)); + } catch (ClassNotFoundException e) {} + + // try T-Store if iap_plugin-dev.jar with T-Store SDK is compiled with app + try { + TStore.class.getClassLoader().loadClass("com.skplanet.dodo.IapPlugin"); + stores2check.add(new TStore(context, options.storeKeys.get(OpenIabHelper.NAME_TSTORE))); + } catch (ClassNotFoundException e) {} + + if (getAllStoreSkus(NAME_SAMSUNG).size() > 0) { + // SamsungApps shows lot of UI stuff during init + // try it only if samsung SKUs are specified + stores2check.add(new SamsungApps(activity, options)); + } + //Nokia TODO change logic + stores2check.add(new NokiaStore(context)); + if (!hasRequestedPermission(context, "com.nokia.payment.BILLING")) { + if (isDebugLog()) { + Log.w(TAG, "Required permission \"com.nokia.payment.BILLING\" NOT REQUESTED"); + } + } + } + + //todo redo + boolean hasFortumoInSetup; + if (BuildConfig.FORTUMO_ENABLE){ + hasFortumoInSetup = false; + for (Appstore store : stores2check) { + if (store instanceof SamsungApps) { + samsungInSetup = (SamsungApps) store; + } else if (store instanceof FortumoStore) { + hasFortumoInSetup = true; + } + } + } + + IabResult result = new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, "Billing isn't supported"); + + if (options.checkInventory) { + + final List equippedStores = checkInventory(stores2check); + + if (equippedStores.size() > 0) { + mAppstore = selectBillingService(equippedStores); + } + if (BuildConfig.FORTUMO_ENABLE && mAppstore == null) { + if (!hasFortumoInSetup && options.supportFortumo) { + mAppstore = FortumoStore.initFortumoStore(context, true); + } + } + if (isDebugLog()) Log.d(TAG, in() + " " + "select equipped"); + if (mAppstore != null) { + final String message = "Successfully initialized with existing inventory: " + mAppstore.getAppstoreName(); + result = new IabResult(BILLING_RESPONSE_RESULT_OK, message); + if (isDebugLog()) { + Log.d(TAG, message); + } + } else { + // found no equipped stores. Select store based on store parameters + mAppstore = selectBillingService(stores2check); + if (BuildConfig.FORTUMO_ENABLE && mAppstore == null) { + if (!hasFortumoInSetup && options.supportFortumo) { + mAppstore = FortumoStore.initFortumoStore(context, false); + } + } + if (isDebugLog()) Log.d(TAG, in() + " " + "select non-equipped"); + if (mAppstore != null) { + final String message = "Successfully initialized with non-equipped store: " + mAppstore.getAppstoreName(); + result = new IabResult(BILLING_RESPONSE_RESULT_OK, message); + if (isDebugLog()) { + Log.d(TAG, message); + } + } + } + if (mAppstore != null) { + mAppstoreBillingService = mAppstore.getInAppBillingService(); + } + fireSetupFinished(listener, result); + } else { // no inventory check. Select store based on store parameters + mAppstore = selectBillingService(stores2check); + if (BuildConfig.FORTUMO_ENABLE && null == mAppstore) { + if (!hasFortumoInSetup && options.supportFortumo) { + mAppstore = FortumoStore.initFortumoStore(context, false); + } + } + if (mAppstore != null) { + mAppstoreBillingService = mAppstore.getInAppBillingService(); + mAppstoreBillingService.startSetup(new OnIabSetupFinishedListener() { + public void onIabSetupFinished(IabResult result) { + fireSetupFinished(listener, result); + } + }); + } else { + fireSetupFinished(listener, result); + } + } + for (Appstore store : stores2check) { + if (store != mAppstore && store.getInAppBillingService() != null) { + store.getInAppBillingService().dispose(); + if (isDebugLog()) Log.d(TAG, in() + " " + "startSetup() disposing " + store.getAppstoreName()); + } + } + } + }, "openiab-setup").start(); + } + + /** + * Must be called after setup is finished. See {@link #startSetup(OnIabSetupFinishedListener)} + * @return null if no appstore connected, otherwise name of Appstore OpenIAB has connected to. + */ + public synchronized String getConnectedAppstoreName() { + if (mAppstore == null) return null; + return mAppstore.getAppstoreName(); + } + + /** Check options are valid */ + public static void checkOptions(Options options) { + if (options.verifyMode != Options.VERIFY_SKIP && options.storeKeys != null) { // check publicKeys. Must be not null and valid + for (Entry entry : options.storeKeys.entrySet()) { + if (entry.getValue() == null) { + throw new IllegalArgumentException("Null publicKey for store: " + entry.getKey() + ", key: " + entry.getValue()); + } + try { + Security.generatePublicKey(entry.getValue()); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid publicKey for store: " + entry.getKey() + ", key: " + entry.getValue(), e); + } + } + } + } + + private static void checkSettings(Options options, Context context){ + checkOptions(options); + checkSamsung(context); + if (BuildConfig.FORTUMO_ENABLE) { + checkFortumo(options, context); + } + checkNokia(options, context); + } + + private static void checkFortumo(Options options, Context context) { + if (BuildConfig.FORTUMO_ENABLE) { + boolean checkFortumo = options.supportFortumo; + if (!checkFortumo && options.availableStores != null) { + for (Appstore store : options.availableStores) { + if (store instanceof FortumoStore) { + checkFortumo = true; + break; + } + } + } + if (checkFortumo) { + StringBuilder resultBuilder = new StringBuilder(); + //is Fortumo lib available? + StringBuilder jarResultBuilder = new StringBuilder(); + try { + FortumoStore.class.getClassLoader().loadClass("mp.MpUtils"); + } catch (ClassNotFoundException e) { + jarResultBuilder.append(" \n - Fortumo classes CAN'T be loaded."); + } + + //manifest + StringBuilder manifestResultBuilder = new StringBuilder(); + checkPermission(context, "android.permission.INTERNET", manifestResultBuilder); + checkPermission(context, "android.permission.ACCESS_NETWORK_STATE", manifestResultBuilder); + checkPermission(context, "android.permission.READ_PHONE_STATE", manifestResultBuilder); + // checkPermission(context, "android.permission.RECEIVE_SMS", manifestResultBuilder); + // checkPermission(context, "android.permission.SEND_SMS", manifestResultBuilder); + + Intent paymentActivityIntent = new Intent(); + paymentActivityIntent.setClassName(context.getPackageName(), "mp.MpActivity"); + if (context.getPackageManager().resolveActivity(paymentActivityIntent, 0) == null) { + formatComponentStatus(" - Required mp.MpActivity is NOT declared.", manifestResultBuilder); + } + + Intent mpServerIntent = new Intent(); + mpServerIntent.setClassName(context.getPackageName(), "mp.MpService"); + if (context.getPackageManager().resolveService(mpServerIntent, 0) == null) { + formatComponentStatus(" - Required mp.MpService is NOT declared.", manifestResultBuilder); + } + + Intent statusUpdateServiceIntent = new Intent(); + statusUpdateServiceIntent.setClassName(context.getPackageName(), "mp.StatusUpdateService"); + if (context.getPackageManager().resolveService(statusUpdateServiceIntent, 0) == null) { + formatComponentStatus(" - Required mp.StatusUpdateService is NOT declared.", manifestResultBuilder); + } + + //xml + StringBuilder xmlStringBuilder = new StringBuilder(); + try { + final List strings = Arrays.asList(context.getResources().getAssets().list("")); + final boolean hasProductFile = strings.contains(FortumoStore.IN_APP_PRODUCTS_FILE_NAME); + final boolean hasFortumoDetailsFile = strings.contains(FortumoStore.FORTUMO_DETAILS_FILE_NAME); + if (!hasProductFile) { + xmlStringBuilder.append(" - Required file " + FortumoStore.IN_APP_PRODUCTS_FILE_NAME + " NOT found in /assets."); + } + if (!hasFortumoDetailsFile) { + if (!hasProductFile) { + xmlStringBuilder.append('\n'); + } + xmlStringBuilder.append(" - Required file " + FortumoStore.FORTUMO_DETAILS_FILE_NAME + " NOT found in /assets."); + } + } catch (IOException e) { + if (xmlStringBuilder.length() > 0) { + xmlStringBuilder.append('\n'); + } + xmlStringBuilder.append("- Xml files CANNOT be parsed."); + } + + final boolean noJar = jarResultBuilder.length() > 0; + final boolean smthWrongWithManifest = manifestResultBuilder.length() > 0; + final boolean smthWrongWithgXmlFiles = xmlStringBuilder.length() > 0; + if (noJar || smthWrongWithManifest || smthWrongWithgXmlFiles) { + resultBuilder.append("\nFortumo setup failed for the following reasons:"); + if (noJar) { + resultBuilder.append('\n'); + resultBuilder.append(jarResultBuilder); + } + if (smthWrongWithgXmlFiles) { + resultBuilder.append('\n'); + resultBuilder.append(xmlStringBuilder); + } + if(smthWrongWithManifest){ + resultBuilder.append('\n'); + resultBuilder.append(manifestResultBuilder); + } + } + if (resultBuilder.length() > 0) { + resultBuilder.append('\n') + .append("********************************************************************************************************\n") + .append("* To support Fortumo follow the instructions of https://github.com/onepf/OpenIAB/blob/master/README.md *\n") + .append("********************************************************************************************************"); + throw new IllegalStateException(resultBuilder.toString(), null); + } + } + } + + } + + private static void checkNokia(Options options, Context context) { + List availableStores = options.availableStores; + boolean hasNokia = false; + if (availableStores != null && !availableStores.isEmpty()) { + for (Appstore appstore : availableStores) { + if (appstore.getAppstoreName().equals(NAME_NOKIA)) { + hasNokia = true; + break; + } + } + } + if (hasNokia) { + if (!hasRequestedPermission(context, "com.nokia.payment.BILLING")) { + throw new IllegalStateException("Nokia permission \"com.nokia.payment.BILLING\" NOT REQUESTED"); + } + } + } + + //todo move to Utils + private static boolean hasRequestedPermission(Context context, String permission) { + try { + PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS); + if (info.requestedPermissions != null) { + for (String p : info.requestedPermissions) { + if (p.equals(permission)) { + return true; + } + } + } + } catch (NameNotFoundException e) { + if (isDebugLog()) { + Log.e(TAG, "error during checking permissions", e); + } + } + return false; + } + + //todo move to Utils + private static void checkPermission(Context context, String paramString, StringBuilder builder) { + if (context.checkCallingOrSelfPermission(paramString) != PackageManager.PERMISSION_GRANTED) { + if (builder.length() > 0) { + builder.append('\n'); + } + builder.append(String.format(" - Required permission \"%s\" is NOT granted.", paramString)); + } + } + + //todo move to Utils + private static void formatComponentStatus(String message, StringBuilder messageBuilder){ + if (messageBuilder.length() > 0) { + messageBuilder.append('\n'); + } + messageBuilder.append(message); + } + + + + private static void checkSamsung(Context context) { + List allStoreSkus = getAllStoreSkus(OpenIabHelper.NAME_SAMSUNG); + if (!allStoreSkus.isEmpty()) { // it means that Samsung is among the candidates + for (String sku : allStoreSkus) { + SamsungApps.checkSku(sku); + } + if (!(context instanceof Activity)) { + // + // Unfortunately, SamsungApps requires to launch their own "Certification Activity" + // in order to connect to billing service. So it's also needed for OpenIAB. + // + // Because of SKU for SamsungApps are specified, + // intance of Activity needs to be passed to OpenIAB constructor to launch + // Samsung Cerfitication Activity. + // Activity also need to pass activityResult to OpenIABHelper.handleActivityResult() + // + // + throw new IllegalArgumentException( + "\n " + + "\nContext is not instance of Activity." + + "\nUnfortunately, SamsungApps requires to launch their own Certification Activity " + + "\nin order to connect to billing service. So it's also needed for OpenIAB." + + "\n " + + "\nBecause of SKU for SamsungApps are specified, instance of Activity needs to be passed " + + "\nto OpenIAB constructor to launch Samsung Cerfitication Activity." + + "\nActivity should call OpenIabHelper#handleActivityResult()." + + "\n "); + } + } + } + + protected void fireSetupFinished(final IabHelper.OnIabSetupFinishedListener listener, final IabResult result) { + if (setupState == SETUP_DISPOSED) return; + if (isDebugLog()) Log.d(TAG, in() + " " + "fireSetupFinished() === SETUP DONE === result: " + result + + (mAppstore != null ? ", appstore: " + mAppstore.getAppstoreName() : "")); + + samsungInSetup = null; + setupState = result.isSuccess() ? SETUP_RESULT_SUCCESSFUL : SETUP_RESULT_FAILED; + notifyHandler.post(new Runnable() { + public void run() { + listener.onIabSetupFinished(result); + } + }); + } + + /** + * Discover all OpenStore services, checks them and build {@link #availableStores} list
    + *

    + * Lock current thread for {@link Options#discoveryTimeoutMs}
    + * Must not be called from main thread to avoid service connection blocking + * + * @param dest - discovered OpenStores will be added here. If null new List() will be created + * @param options - settings for Appstore discovery like verifyMode and timeouts + * + * @return dest or new List with discovered Appstores + */ + public static List discoverOpenStores(final Context context, final List dest, final Options options) { + if (Thread.currentThread().equals(Looper.getMainLooper().getThread())) { + throw new IllegalStateException("Must not be called from main thread. " + + "Service interaction will be blocked"); + } + PackageManager packageManager = context.getPackageManager(); + final Intent intentAppstoreServices = new Intent(BIND_INTENT); + List infoList = packageManager.queryIntentServices(intentAppstoreServices, 0); + final List result = dest != null ? dest : new ArrayList(infoList != null ? infoList.size() : 0); + if (infoList == null || infoList.isEmpty()) { + return result; + } + final CountDownLatch storesToCheck = new CountDownLatch(infoList.size()); + for (ResolveInfo info : infoList) { + String packageName = info.serviceInfo.packageName; + String name = info.serviceInfo.name; + Intent intentAppstore = new Intent(intentAppstoreServices); + intentAppstore.setClassName(packageName, name); + try { + boolean isBound = context.bindService(intentAppstore, new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + if (isDebugLog()) Log.d(TAG, "discoverOpenStores() appstoresService connected for component: " + name.flattenToShortString()); + IOpenAppstore openAppstoreService = IOpenAppstore.Stub.asInterface(service); + + try { + String appstoreName = openAppstoreService.getAppstoreName(); + Intent billingIntent = openAppstoreService.getBillingServiceIntent(); + if (appstoreName == null) { // no name - no service + Log.e(TAG, "discoverOpenStores() Appstore doesn't have name. Skipped. ComponentName: " + name); + } else if (billingIntent == null) { // don't handle stores without billing support + if (isDebugLog()) Log.d(TAG, "discoverOpenStores(): billing is not supported by store: " + name); + } else if ((options.verifyMode == Options.VERIFY_EVERYTHING) && !options.storeKeys.containsKey(appstoreName)) { + // don't connect to OpenStore if no key provided and verification is strict + Log.e(TAG, "discoverOpenStores() verification is required but publicKey is not provided: " + name); + } else { + String publicKey = options.storeKeys.get(appstoreName); + if (options.verifyMode == Options.VERIFY_SKIP) publicKey = null; + final OpenAppstore openAppstore = new OpenAppstore(context, appstoreName, openAppstoreService, billingIntent, publicKey, this); + openAppstore.componentName = name; + Log.d(TAG, "discoverOpenStores() add new OpenStore: " + openAppstore); + synchronized (result) { + if (result.contains(openAppstore) == false) { + result.add(openAppstore); + } + } + } + } catch (RemoteException e) { + Log.e(TAG, "discoverOpenStores() ComponentName: " + name, e); + } + storesToCheck.countDown(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + if (isDebugLog()) Log.d(TAG, "onServiceDisconnected() appstoresService disconnected for component: " + name.flattenToShortString()); + //Nothing to do here + } + }, Context.BIND_AUTO_CREATE); + if (!isBound) { + storesToCheck.countDown(); + } + }catch (SecurityException e){ + Log.e(TAG, "bindService() failed for " + packageName, e); + storesToCheck.countDown(); + } + } + try { + storesToCheck.await(options.discoveryTimeoutMs, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Log.e(TAG, "Interrupted: discovering OpenStores. ", e); + } + return result; + } + + /** + * Connects to Billing Service of each store. Request list of user purchases (inventory) + * + * @see {@link OpenIabHelper#INVENTORY_CHECK_TIMEOUT_MS} to set timout value + * + * @param availableStores - list of stores to check + * @return list of stores with non-empty inventory + */ + protected List checkInventory(final List availableStores) { + String packageName = context.getPackageName(); + // candidates: + Map candidates = new HashMap(); + for (Appstore appstore : availableStores) { + if (appstore.isBillingAvailable(packageName)) { + candidates.put(appstore.getAppstoreName(), appstore); + } + } + if (isDebugLog()) Log.d(TAG, in() + " " + candidates.size() + " inventory candidates"); + final List equippedStores = Collections.synchronizedList(new ArrayList()); + final CountDownLatch storeRemains = new CountDownLatch(candidates.size()); + // for every appstore: connect to billing service and check inventory + for (Map.Entry entry : candidates.entrySet()) { + final Appstore appstore = entry.getValue(); + final AppstoreInAppBillingService billingService = entry.getValue().getInAppBillingService(); + billingService.startSetup(new OnIabSetupFinishedListener() { + public void onIabSetupFinished(IabResult result) { + if (isDebugLog()) Log.d(TAG, in() + " " + "billing set " + appstore.getAppstoreName()); + if(result.isFailure()) { + storeRemains.countDown(); + return; + } + new Thread(new Runnable() { + public void run() { + try { + Inventory inventory = billingService.queryInventory(false, null, null); + if (inventory.getAllPurchases().size() > 0) { + equippedStores.add(appstore); + } + if (isDebugLog()) Log.d(TAG, in() + " " + "inventoryCheck() in " + appstore.getAppstoreName() + " found: " + inventory.getAllPurchases().size() + " purchases"); + } catch (IabException e) { + Log.e(TAG, "inventoryCheck() failed for " + appstore.getAppstoreName()); + } + storeRemains.countDown(); + } + }, "inv-check[" + appstore.getAppstoreName()+ "]").start(); + } + }); + } + try { + storeRemains.await(options.checkInventoryTimeoutMs, TimeUnit.MILLISECONDS); + if (isDebugLog()) Log.d(TAG, in() + " " + "inventory check done"); + } catch (InterruptedException e) { + Log.e(TAG, "selectBillingService() inventory check is failed. candidates: " + candidates.size() + + ", inventory remains: " + storeRemains.getCount() , e); + } + return equippedStores; + } + + /** + * Lookup for requested service in store based on isPackageInstaller() & isBillingAvailable() + *

    + * Scenario: + *

  • + * - look for installer: if exists and supports billing service - we done
  • + * - rest of stores who support billing considered as candidates

  • + * + * - find candidate according to [prefferedStoreNames]. if found - we done

  • + * + * - select candidate randomly from 3 groups based on published package version
  • + * - published version == app.versionCode
  • + * - published version > app.versionCode
  • + * - published version < app.versionCode + * + */ + protected Appstore selectBillingService(final List availableStores) { + String packageName = context.getPackageName(); + // candidates: + Map candidates = new HashMap(); + // + for (Appstore appstore : availableStores) { + if (appstore.isBillingAvailable(packageName)) { + candidates.put(appstore.getAppstoreName(), appstore); + } else { + continue; // for billing we cannot select store without billing + } + if (appstore.isPackageInstaller(packageName)) { + return appstore; + } + } + if (candidates.size() == 0) return null; + + // lookup for developer preffered stores + for (int i = 0; i < options.prefferedStoreNames.length; i++) { + Appstore candidate = candidates.get(options.prefferedStoreNames[i]); + if (candidate != null) { + return candidate; + } + } + // nothing found. select something that matches package version + int versionCode = Appstore.PACKAGE_VERSION_UNDEFINED; + try { + versionCode = context.getPackageManager().getPackageInfo(packageName, 0).versionCode; + } catch (NameNotFoundException e) { + Log.e(TAG, "Are we installed?", e); + } + List sameVersion = new ArrayList(); + List higherVersion = new ArrayList(); + for (Appstore candidate : candidates.values()) { + final int storeVersion = candidate.getPackageVersion(packageName); + if (storeVersion == versionCode) { + sameVersion.add(candidate); + } else if (storeVersion > versionCode) { + higherVersion.add(candidate); + } + } + // use random if found stores with same version of package + if (sameVersion.size() > 0) { + return sameVersion.get(new Random().nextInt(sameVersion.size())); + } else if (higherVersion.size() > 0) { // or one of higher version + return higherVersion.get(new Random().nextInt(higherVersion.size())); + } else { // ok, return no matter what + return new ArrayList(candidates.values()).get(new Random().nextInt(candidates.size())); + } + } + + public void dispose() { + logDebug("Disposing."); + if (mAppstoreBillingService != null) { + mAppstoreBillingService.dispose(); + } + setupState = SETUP_DISPOSED; + } + + public boolean subscriptionsSupported() { + checkSetupDone("subscriptionsSupported"); + return mAppstoreBillingService.subscriptionsSupported(); + } + + public void launchPurchaseFlow(Activity act, String sku, int requestCode, IabHelper.OnIabPurchaseFinishedListener listener) { + launchPurchaseFlow(act, sku, requestCode, listener, ""); + } + + public void launchPurchaseFlow(Activity act, String sku, int requestCode, + IabHelper.OnIabPurchaseFinishedListener listener, String extraData) { + launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData); + } + + public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, + IabHelper.OnIabPurchaseFinishedListener listener) { + launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, ""); + } + + public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, + IabHelper.OnIabPurchaseFinishedListener listener, String extraData) { + launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData); + } + + public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode, + IabHelper.OnIabPurchaseFinishedListener listener, String extraData) { + checkSetupDone("launchPurchaseFlow"); + String storeSku = getStoreSku(mAppstore.getAppstoreName(), sku); + mAppstoreBillingService.launchPurchaseFlow(act, storeSku, itemType, requestCode, listener, extraData); + } + + public boolean handleActivityResult(int requestCode, int resultCode, Intent data) { + if (isDebugLog()) Log.d(TAG, in() + " " + "handleActivityResult() requestCode: " + requestCode+ " resultCode: " + resultCode+ " data: " + data); + if (requestCode == options.samsungCertificationRequestCode && samsungInSetup != null) { + return samsungInSetup.getInAppBillingService().handleActivityResult(requestCode, resultCode, data); + } + if (setupState != SETUP_RESULT_SUCCESSFUL) { + if (isDebugLog()) Log.d(TAG, "handleActivityResult() setup is not done. requestCode: " + requestCode+ " resultCode: " + resultCode+ " data: " + data); + return false; + } + return mAppstoreBillingService.handleActivityResult(requestCode, resultCode, data); + } + + /** + * See {@link #queryInventory(boolean, List, List)} for details + */ + public Inventory queryInventory(boolean querySkuDetails, List 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 {@link #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. + * @throws IabException if a problem occurs while refreshing the inventory. + */ + public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus, List moreSubsSkus) throws IabException { + checkSetupDone("queryInventory"); + + List moreItemStoreSkus = null; + if (moreItemSkus != null) { + moreItemStoreSkus = new ArrayList(); + for (String sku : moreItemSkus) { + String storeSku = getStoreSku(mAppstore.getAppstoreName(), sku); + moreItemStoreSkus.add(storeSku); + } + } + List moreSubsStoreSkus = null; + if (moreSubsSkus != null) { + moreSubsStoreSkus = new ArrayList(); + for (String sku : moreSubsSkus) { + String storeSku = getStoreSku(mAppstore.getAppstoreName(), sku); + moreSubsStoreSkus.add(storeSku); + } + } + return mAppstoreBillingService.queryInventory(querySkuDetails, moreItemStoreSkus, moreSubsStoreSkus); + } + + /** + * Queries the inventory. This will query all owned items from the server, as well as + * information on additional skus, if specified. This method may block or take long to execute. + * Do not call from a UI thread. For that, use the non-blocking version {@link #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. + * @throws IabException if a problem occurs while refreshing the inventory. + */ + public void queryInventoryAsync(final boolean querySkuDetails, final List moreItemSkus, final List moreSubsSkus, final IabHelper.QueryInventoryFinishedListener listener) { + checkSetupDone("queryInventory"); + if (listener == null) { + throw new IllegalArgumentException("Inventory listener must be not null"); + } + flagStartAsync("refresh inventory"); + (new Thread(new Runnable() { + 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 (setupState != SETUP_DISPOSED) { + notifyHandler.post(new Runnable() { + public void run() { + listener.onQueryInventoryFinished(result_f, inv_f); + } + }); + } + } + })).start(); + } + + /** + * For details see {@link #queryInventoryAsync(boolean, List, List, QueryInventoryFinishedListener)} + */ + public void queryInventoryAsync(final boolean querySkuDetails, final List moreSkus, final IabHelper.QueryInventoryFinishedListener listener) { + checkSetupDone("queryInventoryAsync"); + if (listener == null) { + throw new IllegalArgumentException("Inventory listener must be not null!"); + } + queryInventoryAsync(querySkuDetails, moreSkus, null, listener); + } + + /** + * For details see {@link #queryInventoryAsync(boolean, List, List, QueryInventoryFinishedListener)} + */ + public void queryInventoryAsync(IabHelper.QueryInventoryFinishedListener listener) { + checkSetupDone("queryInventoryAsync"); + if (listener == null) { + throw new IllegalArgumentException("Inventory listener must be not null!"); + } + queryInventoryAsync(true, null, listener); + } + + /** + * For details see {@link #queryInventoryAsync(boolean, List, List, QueryInventoryFinishedListener)} + */ + public void queryInventoryAsync(boolean querySkuDetails, IabHelper.QueryInventoryFinishedListener listener) { + checkSetupDone("queryInventoryAsync"); + if (listener == null) { + throw new IllegalArgumentException("Inventory listener must be not null!"); + } + queryInventoryAsync(querySkuDetails, null, listener); + } + + public void consume(Purchase itemInfo) throws IabException { + checkSetupDone("consume"); + Purchase purchaseStoreSku = (Purchase) itemInfo.clone(); // TODO: use Purchase.getStoreSku() + purchaseStoreSku.setSku(getStoreSku(mAppstore.getAppstoreName(), itemInfo.getSku())); + mAppstoreBillingService.consume(purchaseStoreSku); + } + + public void consumeAsync(Purchase purchase, IabHelper.OnConsumeFinishedListener listener) { + checkSetupDone("consumeAsync"); + if (listener == null) { + throw new IllegalArgumentException("Consume listener must be not null!"); + } + List purchases = new ArrayList(); + purchases.add(purchase); + consumeAsyncInternal(purchases, listener, null); + } + + public void consumeAsync(List purchases, IabHelper.OnConsumeMultiFinishedListener listener) { + checkSetupDone("consumeAsync"); + if (listener == null) { + throw new IllegalArgumentException("Consume listener must be not null!"); + } + consumeAsyncInternal(purchases, null, listener); + } + + void consumeAsyncInternal(final List purchases, + final IabHelper.OnConsumeFinishedListener singleListener, + final IabHelper.OnConsumeMultiFinishedListener multiListener) { + checkSetupDone("consume"); + flagStartAsync("consume"); + (new Thread(new Runnable() { + public void run() { + final List 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 (setupState != SETUP_DISPOSED && singleListener != null) { + notifyHandler.post(new Runnable() { + public void run() { + singleListener.onConsumeFinished(purchases.get(0), results.get(0)); + } + }); + } + if (setupState != SETUP_DISPOSED && multiListener != null) { + notifyHandler.post(new Runnable() { + public void run() { + multiListener.onConsumeMultiFinished(purchases, results); + } + }); + } + } + })).start(); + } + + // Checks that setup was done; if not, throws an exception. + void checkSetupDone(String operation) { + String stateToString = setupStateToString(setupState); + if (setupState != SETUP_RESULT_SUCCESSFUL) { + logError("Illegal state for operation (" + operation + "): " + stateToString); + throw new IllegalStateException(stateToString + " Can't perform operation: " + operation); + } + } + + void flagStartAsync(String operation) { + // TODO: why can't be called consume and queryInventory at the same time? +// 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); + } + + void flagEndAsync() { + logDebug("Ending async operation: " + mAsyncOperation); + mAsyncOperation = ""; + mAsyncInProgress = false; + } + + void logDebug(String msg) { + if (isDebugLog()) Log.d(TAG, msg); + } + + void logError(String msg) { + Log.e(TAG, "In-app billing error: " + msg); + } + + void logWarn(String msg) { + if (isDebugLog()) Log.w(TAG, "In-app billing warning: " + msg); + } + + private static String setupStateToString(int setupState) { + String state; + if (setupState == SETUP_RESULT_NOT_STARTED) { + state = " IAB helper is not set up."; + } else if (setupState == SETUP_DISPOSED) { + state = "IAB helper was disposed of."; + } else if (setupState == SETUP_RESULT_SUCCESSFUL) { + state = "IAB helper is set up."; + } else if (setupState == SETUP_RESULT_FAILED) { + state = "IAB helper setup failed."; + } else { + throw new IllegalStateException("Wrong setup state: " + setupState); + } + return state; + } + + public interface OnInitListener { + public void onInitFinished(); + } + + public interface OnOpenIabHelperInitFinished { + public void onOpenIabHelperInitFinished(); + } + + private static String in() { + return "in: " + (System.currentTimeMillis() - started); + } + + public static boolean isDebugLog() { + return OpenIabHelper.isDebugLog || Log.isLoggable(TAG, Log.DEBUG); + } + + public static void enableDebugLogging(boolean enabled) { + OpenIabHelper.isDebugLog = enabled; + } + + public static void enableDebugLogging(boolean enabled, String tag) { + OpenIabHelper.isDebugLog = enabled; + OpenIabHelper.TAG = tag; + } + + public static boolean isPackageInstaller(Context appContext, String installer) { + String installerPackageName = appContext.getPackageManager().getInstallerPackageName(appContext.getPackageName()); + return installerPackageName != null && installerPackageName.equals(installer); + } + + /** + * All options of OpenIAB can be found here + * + * TODO: consider to use cloned instance of Options in OpenIABHelper + */ + public static class Options { + + /** + * List of stores to be used for store elections. By default GooglePlay, Amazon, SamsungApps and + * all installed OpenStores are used. + *

    + * To specify your own list, you need to instantiate Appstore object manually. + * GooglePlay, Amazon and SamsungApps could be instantiated directly. OpenStore can be discovered + * using {@link OpenIabHelper#discoverOpenStores(Context, List, Options)} + *

    + * If you put only your instance of Appstore in this list OpenIAB will use it + * + * TODO: consider to use AppstoreFactory.get(storeName) -> Appstore instance + */ + public List availableStores; + + /** + * Wait specified amount of ms to find all OpenStores on device + */ + public int discoveryTimeoutMs = DISCOVER_TIMEOUT_MS; + /** + * Check user inventory in every store to select proper store + *

    + * Will try to connect to each billingService and extract user's purchases. + * If purchases have been found in the only store that store will be used for further purchases. + * If purchases have been found in multiple stores only such stores will be used for further elections + */ + public boolean checkInventory = true; + + /** + * Wait specified amount of ms to check inventory in all stores + */ + public int checkInventoryTimeoutMs = INVENTORY_CHECK_TIMEOUT_MS; + + /** + * OpenIAB could skip receipt verification by publicKey for GooglePlay and OpenStores + *

    + * Receipt could be verified in {@link OnIabPurchaseFinishedListener#onIabPurchaseFinished()} + * using {@link Purchase#getOriginalJson()} and {@link Purchase#getSignature()} + */ + public int verifyMode = VERIFY_EVERYTHING; + /** + * Verify signatures in any store. + *

    + * By default in Google's IabHelper. Throws exception if key is not available or invalid. + * To prevent crashes OpenIAB wouldn't connect to OpenStore if no publicKey provided + */ + public static final int VERIFY_EVERYTHING = 0; + /** + * Don't verify signatires. To perform verification on server-side + */ + public static final int VERIFY_SKIP = 1; + /** + * Verify signatures only if publicKey is available. Otherwise skip verification. + *

    + * Developer is responsible for verify + */ + public static final int VERIFY_ONLY_KNOWN = 2; + + /** + * storeKeys is map of [ appstore name -> publicKeyBase64 ] + * Put keys for all stores you support in this Map and pass it to instantiate {@link OpenIabHelper} + *

    + * publicKey key is used to verify receipt is created by genuine Appstore using + * provided signature. It can be found in Developer Console of particular store + *

    + * name of particular store can be provided by local_store tool if you run it on device. + * For Google Play OpenIAB uses {@link OpenIabHelper#NAME_GOOGLE}. + *

    + *

    Note: + * AmazonApps and SamsungApps doesn't use RSA keys for receipt verification, so you don't need + * to specify it + */ + public Map storeKeys = new HashMap(); + + /** + * Used as priority list if store that installed app is not found and there are + * multiple stores installed on device that supports billing. + */ + public String[] prefferedStoreNames = new String[] {}; + + /** Used for SamsungApps setup. Specify your own value if default one interfere your code. + *

    default value is {@link SamsungAppsBillingService#REQUEST_CODE_IS_ACCOUNT_CERTIFICATION} */ + public int samsungCertificationRequestCode = SamsungAppsBillingService.REQUEST_CODE_IS_ACCOUNT_CERTIFICATION; + + /** + * Is Fortumo supported? + * + */ + public boolean supportFortumo; + + } + +} diff --git a/library/src/org/onepf/oms/appstore/nokiaUtils/NokiaStoreHelper.java b/library/src/org/onepf/oms/appstore/nokiaUtils/NokiaStoreHelper.java index 583a28a6..e4510b8d 100644 --- a/library/src/org/onepf/oms/appstore/nokiaUtils/NokiaStoreHelper.java +++ b/library/src/org/onepf/oms/appstore/nokiaUtils/NokiaStoreHelper.java @@ -34,7 +34,7 @@ import java.util.List; public class NokiaStoreHelper implements AppstoreInAppBillingService { - + //todo why own constants? public static final int RESULT_OK = 0; public static final int RESULT_USER_CANCELED = 1; public static final int RESULT_BILLING_UNAVAILABLE = 3; @@ -45,6 +45,8 @@ public class NokiaStoreHelper implements AppstoreInAppBillingService { public static final int RESULT_ITEM_NOT_OWNED = 8; public static final int RESULT_NO_SIM = 9; + public static final int RESULT_BAD_RESPONSE = -1002; + private static final String TAG = NokiaStoreHelper.class.getSimpleName(); public static final boolean IS_DEBUG_MODE = false; @@ -128,12 +130,18 @@ public void onServiceDisconnected(final ComponentName name) { "Billing service unavailable on device.")); } - } else { - - mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE); - - } - } + } else { + try { + mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE); + } catch (SecurityException e) { + logError("Can't bind to the service", e); + if (listener != null) { + listener.onIabSetupFinished(new NokiaResult(RESULT_BILLING_UNAVAILABLE, + "Billing service unavailable on device due to lack of the permission \"com.nokia.payment.BILLING\".")); + } + } + } + } private Intent getServiceIntent() { final Intent intent = new Intent(NokiaStore.VENDING_ACTION); @@ -180,29 +188,36 @@ public void launchPurchaseFlow(final Activity act, final String sku, final Strin } try { - final Bundle buyIntentBundle = mService.getBuyIntent( - 3, getPackageName(), sku, IabHelper.ITEM_TYPE_INAPP, extraData - ); - - logDebug("buyIntentBundle = " + buyIntentBundle); - - final int responseCode = buyIntentBundle.getInt("RESPONSE_CODE", 0); - final PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT"); - - if (responseCode == RESULT_OK) { - mRequestCode = requestCode; - mPurchaseListener = listener; - - final IntentSender intentSender = pendingIntent.getIntentSender(); - act.startIntentSenderForResult( - intentSender, requestCode, new Intent(), 0, 0, 0 - ); - } else if(listener != null) { - final IabResult result = new NokiaResult(responseCode, "Failed to get buy intent."); - listener.onIabPurchaseFinished(result, null); - } - - } catch (RemoteException e) { + if (mService == null) { + if (listener != null) { + logError("Unable to buy item, Error response: service is not connected."); + NokiaResult result = new NokiaResult(RESULT_ERROR, "Unable to buy item"); + listener.onIabPurchaseFinished(result, null); + } + } else { + final Bundle buyIntentBundle = mService.getBuyIntent( + 3, getPackageName(), sku, IabHelper.ITEM_TYPE_INAPP, extraData + ); + + logDebug("buyIntentBundle = " + buyIntentBundle); + + final int responseCode = buyIntentBundle.getInt("RESPONSE_CODE", 0); + final PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT"); + + if (responseCode == RESULT_OK) { + mRequestCode = requestCode; + mPurchaseListener = listener; + + final IntentSender intentSender = pendingIntent.getIntentSender(); + act.startIntentSenderForResult( + intentSender, requestCode, new Intent(), 0, 0, 0 + ); + } else if (listener != null) { + final IabResult result = new NokiaResult(responseCode, "Failed to get buy intent."); + listener.onIabPurchaseFinished(result, null); + } + } + } catch (RemoteException e) { logError("RemoteException: " + e, e); final IabResult result = new NokiaResult(IabHelper.IABHELPER_SEND_INTENT_FAILED, "Failed to send intent."); @@ -460,6 +475,11 @@ private void refreshPurchasedItems(final List moreItemSkus, final Invent storeSkusBundle.putStringArrayList("ITEM_ID_LIST", storeSkus); try { + if (mService == null) { + logError("Unable to refresh purchased items."); + throw new IabException(RESULT_BAD_RESPONSE, "Error refreshing inventory (querying owned items)."); + } + final Bundle purchasedBundle = mService.getPurchases( 3, getPackageName(), OpenIabHelper.ITEM_TYPE_INAPP, storeSkusBundle, null ); @@ -535,8 +555,12 @@ private void refreshItemDetails(final List moreItemSkus, final Inventory storeSkusBundle.putStringArrayList("ITEM_ID_LIST", combinedStoreSkus); try { + if (mService == null) { + logError("Unable to refresh item details."); + throw new IabException(RESULT_BAD_RESPONSE, "Error refreshing item details."); + } - final Bundle productDetailBundle = mService.getProductDetails( + final Bundle productDetailBundle = mService.getProductDetails( 3, getPackageName(), OpenIabHelper.ITEM_TYPE_INAPP, storeSkusBundle );