From c8e0cd8ab31910b2183539cd9b5e19ea4152f275 Mon Sep 17 00:00:00 2001 From: Gregory Kokanosky Date: Mon, 24 Jul 2017 09:31:13 +0200 Subject: [PATCH] Support for getBuyIntentExtraParams() (#287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for getBuyIntentExtraParams() Add support for getBuyIntentExtraParams() as this is the recommended method by Google https://developer.android.com/google/play/billing/billing_reference.html#getBuyIntent The method is a variant of the getBuyIntent() method, and takes an additional extraParams parameter. More info here: https://developer.android.com/google/play/billing/billing_reference.html#getBuyIntentExtraParams * Cleanup * Address coding style issues * Remove extra blank line * As requested, create specific methods to send the extra params, instead of overriding existing ones * Fix style * Implement @serggl’s feedback Use isBillingSupportedExtraParams() to check if we can use the new getBuyIntentExtraParams() method and fallback to the previous method if that’s not the case. As isBillingSupportedExtraParams() is only available starting with Billing API v7, we effectively limit the usage of getBuyIntentExtraParams() to Billing API v7+ * Add missing javadoc + Fix style * Update README to add new methods usage --- README.md | 14 ++ .../vending/billing/IInAppBillingService.aidl | 95 ++++++++- .../android/iab/v3/BillingProcessor.java | 195 ++++++++++++++++-- .../com/anjlab/android/iab/v3/Constants.java | 4 + 4 files changed, 291 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 7cc27864..84087994 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,20 @@ bp.purchase(YOUR_ACTIVITY, "YOUR PRODUCT ID FROM GOOGLE PLAY CONSOLE HERE", "DEV bp.subscribe(YOUR_ACTIVITY, "YOUR SUBSCRIPTION ID FROM GOOGLE PLAY CONSOLE HERE", "DEVELOPER PAYLOAD HERE"); ``` _IMPORTANT: when you provide a payload, internally the library prepends a string to your payload. For subscriptions, it prepends `"subs:\:"`, and for products, it prepends `"inapp:\:\:"`. This is important to know if you do any validation on the payload returned from Google Play after a successful purchase._ + +_With a bundle of extra parameters:_ + +```java +Bundle extraParams = new Bundle() +extraParams.putString("accountId", "MY_ACCOUNT_ID"); +bp.purchase(YOUR_ACTIVITY, "YOUR PRODUCT ID FROM GOOGLE PLAY CONSOLE HERE", null /*or developer payload*/, extraParams); +bp.subscribe(YOUR_ACTIVITY, "YOUR SUBSCRIPTION ID FROM GOOGLE PLAY CONSOLE HERE", null /*or developer payload*/, extraParams); +``` + +Use these methods if you want to pass extra parameters, [as documented here](https://developer.android.com/google/play/billing/billing_reference.html#getBuyIntentExtraParams), you can provide a Bundle object. + +_Please note that this feature is only available if the target device is support the version 7 of the In App billing API._ + * **That's it! A super small and fast in-app library ever!** * **And don't forget** diff --git a/library/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl b/library/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl index 3d67f0ce..d89d0e73 100644 --- a/library/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl +++ b/library/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012 Google Inc. + * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -108,7 +108,6 @@ interface IInAppBillingService { * "developerPayload":"example developer payload" }' * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that * was signed with the private key of the developer - * TODO: change this to app-specific keys. */ Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, String developerPayload); @@ -185,8 +184,98 @@ interface IInAppBillingService { * "developerPayload":"example developer payload" }' * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that * was signed with the private key of the developer - * TODO: change this to app-specific keys. */ Bundle getBuyIntentToReplaceSkus(int apiVersion, String packageName, in List oldSkus, String newSku, String type, String developerPayload); + + /** + * Returns a pending intent to launch the purchase flow for an in-app item. This method is + * a variant of the {@link #getBuyIntent} method and takes an additional {@code extraParams} + * parameter. This parameter is a Bundle of optional keys and values that affect the + * operation of the method. + * @param apiVersion billing API version that the app is using, must be 6 or later + * @param packageName package name of the calling app + * @param sku the SKU of the in-app item as published in the developer console + * @param type of the in-app item being purchased ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @param developerPayload optional argument to be sent back with the purchase information + * @extraParams a Bundle with the following optional keys: + * "skusToReplace" - List - an optional list of SKUs that the user is + * upgrading or downgrading from. + * Pass this field if the purchase is upgrading or downgrading + * existing subscriptions. + * The specified SKUs are replaced with the SKUs that the user is + * purchasing. Google Play replaces the specified SKUs at the start of + * the next billing cycle. + * "replaceSkusProration" - Boolean - whether the user should be credited for any unused + * subscription time on the SKUs they are upgrading or downgrading. + * If you set this field to true, Google Play swaps out the old SKUs + * and credits the user with the unused value of their subscription + * time on a pro-rated basis. + * Google Play applies this credit to the new subscription, and does + * not begin billing the user for the new subscription until after + * the credit is used up. + * If you set this field to false, the user does not receive credit for + * any unused subscription time and the recurrence date does not + * change. + * Default value is true. Ignored if you do not pass skusToReplace. + * "accountId" - String - an optional obfuscated string that is uniquely + * associated with the user's account in your app. + * If you pass this value, Google Play can use it to detect irregular + * activity, such as many devices making purchases on the same + * account in a short period of time. + * Do not use the developer ID or the user's Google ID for this field. + * In addition, this field should not contain the user's ID in + * cleartext. + * We recommend that you use a one-way hash to generate a string from + * the user's ID, and store the hashed string in this field. + * "vr" - Boolean - an optional flag indicating whether the returned intent + * should start a VR purchase flow. The apiVersion must also be 7 or + * later to use this flag. + */ + Bundle getBuyIntentExtraParams(int apiVersion, String packageName, String sku, + String type, String developerPayload, in Bundle extraParams); + + /** + * Returns the most recent purchase made by the user for each SKU, even if that purchase is + * expired, canceled, or consumed. + * @param apiVersion billing API version that the app is using, must be 6 or later + * @param packageName package name of the calling app + * @param type of the in-app items being requested ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @param continuationToken to be set as null for the first call, if the number of owned + * skus is too large, a continuationToken is returned in the response bundle. + * This method can be called again with the continuation token to get the next set of + * owned skus. + * @param extraParams a Bundle with extra params that would be appended into http request + * query string. Not used at this moment. Reserved for future functionality. + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value: RESULT_OK(0) if success, + * {@link IabHelper#BILLING_RESPONSE_RESULT_*} response codes on failures. + * + * "INAPP_PURCHASE_ITEM_LIST" - ArrayList containing the list of SKUs + * "INAPP_PURCHASE_DATA_LIST" - ArrayList containing the purchase information + * "INAPP_DATA_SIGNATURE_LIST"- ArrayList containing the signatures + * of the purchase information + * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the + * next set of in-app purchases. Only set if the + * user has more owned skus than the current list. + */ + Bundle getPurchaseHistory(int apiVersion, String packageName, String type, + String continuationToken, in Bundle extraParams); + + /** + * This method is a variant of {@link #isBillingSupported}} that takes an additional + * {@code extraParams} parameter. + * @param apiVersion billing API version that the app is using, must be 7 or later + * @param packageName package name of the calling app + * @param type of the in-app item being purchased ("inapp" for one-time purchases and "subs" + * for subscriptions) + * @param extraParams a Bundle with the following optional keys: + * "vr" - Boolean - an optional flag to indicate whether {link #getBuyIntentExtraParams} + * supports returning a VR purchase flow. + * @return RESULT_OK(0) on success and appropriate response code on failures. + */ + int isBillingSupportedExtraParams(int apiVersion, String packageName, String type, + in Bundle extraParams); } \ No newline at end of file diff --git a/library/src/main/java/com/anjlab/android/iab/v3/BillingProcessor.java b/library/src/main/java/com/anjlab/android/iab/v3/BillingProcessor.java index 4df73ad0..f56f0d67 100644 --- a/library/src/main/java/com/anjlab/android/iab/v3/BillingProcessor.java +++ b/library/src/main/java/com/anjlab/android/iab/v3/BillingProcessor.java @@ -78,6 +78,8 @@ public interface IBillingHandler private String developerMerchantId; private boolean isOneTimePurchasesSupported; private boolean isSubsUpdateSupported; + private boolean isSubscriptionExtraParamsSupported; + private boolean isOneTimePurchaseExtraParamsSupported; private class HistoryInitializationTask extends AsyncTask { @@ -303,12 +305,12 @@ public boolean loadOwnedPurchasesFromGoogle() public boolean purchase(Activity activity, String productId) { - return purchase(activity, productId, Constants.PRODUCT_TYPE_MANAGED, null); + return purchase(activity, null, productId, Constants.PRODUCT_TYPE_MANAGED, null); } public boolean subscribe(Activity activity, String productId) { - return purchase(activity, productId, Constants.PRODUCT_TYPE_SUBSCRIPTION, null); + return purchase(activity, null, productId, Constants.PRODUCT_TYPE_SUBSCRIPTION, null); } public boolean purchase(Activity activity, String productId, String developerPayload) @@ -321,6 +323,53 @@ public boolean subscribe(Activity activity, String productId, String developerPa return purchase(activity, productId, Constants.PRODUCT_TYPE_SUBSCRIPTION, developerPayload); } + /*** + * Purchase a product + * + * @param activity the activity calling this method + * @param productId the product id to purchase + * @param extraParamsBundle A bundle object containing extra parameters to pass to + * getBuyIntentExtraParams() + * @see extra + * params documentation on developer.android.com + * @return {@code false} if the billing system is not initialized, {@code productId} is empty + * or if an exception occurs. Will return {@code true} otherwise. + */ + public boolean purchase(Activity activity, String productId, String developerPayload, Bundle extraParams) + { + if(!isOneTimePurchaseWithExtraParamsSupported(extraParams)) + { + return purchase(activity, productId, developerPayload); + } + else + { + return purchase(activity, null, productId, Constants.PRODUCT_TYPE_MANAGED, developerPayload, extraParams); + } + } + + /** + * Subscribe to a product + * + * @param activity the activity calling this method + * @param productId the product id to purchase + * @param extraParamsBundle A bundle object containing extra parameters to pass to getBuyIntentExtraParams() + * @see extra + * params documentation on developer.android.com + * @return {@code false} if the billing system is not initialized, {@code productId} is empty or if an exception occurs. + * Will return {@code true} otherwise. + */ + public boolean subscribe(Activity activity, String productId, String developerPayload, Bundle extraParams) + { + if (!isSubscriptionWithExtraParamsSupported(extraParams)) + { + return purchase(activity, null, productId, Constants.PRODUCT_TYPE_SUBSCRIPTION, developerPayload); + } + else + { + return purchase(activity, null, productId, Constants.PRODUCT_TYPE_SUBSCRIPTION, developerPayload, extraParams); + } + } + public boolean isOneTimePurchaseSupported() { if (isOneTimePurchasesSupported) @@ -364,6 +413,62 @@ public boolean isSubscriptionUpdateSupported() return isSubsUpdateSupported; } + /** + * Check API v7 support for subscriptions + * @param extraParams + * @return {@code true} if the current API supports calling getBuyIntentExtraParams() for + * subscriptions, {@code false} otherwise. + */ + public boolean isSubscriptionWithExtraParamsSupported(Bundle extraParams) + { + if(isSubscriptionExtraParamsSupported) + { + return true; + } + + try + { + int response = + billingService.isBillingSupportedExtraParams(Constants.GOOGLE_API_VR_SUPPORTED_VERSION, + contextPackageName, + Constants.PRODUCT_TYPE_SUBSCRIPTION, extraParams); + isSubscriptionExtraParamsSupported = response == Constants.BILLING_RESPONSE_RESULT_OK; + } + catch (RemoteException e) + { + e.printStackTrace(); + } + return isSubscriptionExtraParamsSupported; + } + + /** + * Check API v7 support for one-time purchases + * @param extraParams + * @return {@code true} if the current API supports calling getBuyIntentExtraParams() for + * one-time purchases, {@code false} otherwise. + */ + public boolean isOneTimePurchaseWithExtraParamsSupported(Bundle extraParams) + { + if(isOneTimePurchaseExtraParamsSupported) + { + return true; + } + + try + { + int response = + billingService.isBillingSupportedExtraParams(Constants.GOOGLE_API_VR_SUPPORTED_VERSION, + contextPackageName, + Constants.PRODUCT_TYPE_MANAGED, extraParams); + isOneTimePurchaseExtraParamsSupported = response == Constants.BILLING_RESPONSE_RESULT_OK; + } + catch (RemoteException e) + { + e.printStackTrace(); + } + return isOneTimePurchaseExtraParamsSupported; + } + /** * Change subscription i.e. upgrade or downgrade * @@ -433,6 +538,35 @@ public boolean updateSubscription(Activity activity, List oldProductIds, return purchase(activity, oldProductIds, productId, Constants.PRODUCT_TYPE_SUBSCRIPTION, developerPayload); } + /** + * + * @param activity the activity calling this method + * @param oldProductIds passing null will act the same as {@link #subscribe(Activity, String)} + * @param productId the new subscription id + * @param developerPayload the developer payload + * @param extraParams A bundle object containing extra parameters to pass to getBuyIntentExtraParams() + * @see extra + * params documentation on developer.android.com + * @return {@code false} if {@code oldProductIds} is not {@code null} AND change subscription + * is not supported. + */ + public boolean updateSubscription(Activity activity, List oldProductIds, + String productId, String developerPayload, Bundle extraParams) + { + if (oldProductIds != null && !isSubscriptionUpdateSupported()) + { + return false; + } + + // if API v7 is not supported, let's fallback to the previous method + if( !isSubscriptionWithExtraParamsSupported(extraParams)) + { + return updateSubscription(activity, oldProductIds, productId, developerPayload); + } + + return purchase(activity, oldProductIds, productId, Constants.PRODUCT_TYPE_SUBSCRIPTION, developerPayload, extraParams); + } + private boolean purchase(Activity activity, String productId, String purchaseType, String developerPayload) { @@ -441,6 +575,12 @@ private boolean purchase(Activity activity, String productId, String purchaseTyp private boolean purchase(Activity activity, List oldProductIds, String productId, String purchaseType, String developerPayload) + { + return purchase(activity, oldProductIds, productId, purchaseType, developerPayload, null); + } + + private boolean purchase(Activity activity, List oldProductIds, String productId, + String purchaseType, String developerPayload, Bundle extraParamsBundle) { if (!isInitialized() || TextUtils.isEmpty(productId) || TextUtils.isEmpty(purchaseType)) { @@ -461,21 +601,48 @@ private boolean purchase(Activity activity, List oldProductIds, String p Bundle bundle; if (oldProductIds != null && purchaseType.equals(Constants.PRODUCT_TYPE_SUBSCRIPTION)) { - bundle = - billingService.getBuyIntentToReplaceSkus(Constants.GOOGLE_API_SUBSCRIPTION_CHANGE_VERSION, - contextPackageName, - oldProductIds, - productId, - purchaseType, - purchasePayload); + if(extraParamsBundle == null) // API v5 + { + bundle = billingService.getBuyIntentToReplaceSkus(Constants.GOOGLE_API_SUBSCRIPTION_CHANGE_VERSION, + contextPackageName, + oldProductIds, + productId, + purchaseType, + purchasePayload); + } + else // API v7+ supported + { + if (extraParamsBundle == null) + { + extraParamsBundle = new Bundle(); + } + + if (!extraParamsBundle.containsKey(Constants.EXTRA_PARAMS_KEY_SKU_TO_REPLACE)) + { + extraParamsBundle.putStringArrayList(Constants.EXTRA_PARAMS_KEY_SKU_TO_REPLACE, + new ArrayList(oldProductIds)); + } + + bundle = billingService.getBuyIntentExtraParams(Constants.GOOGLE_API_VR_SUPPORTED_VERSION, + contextPackageName, productId, purchaseType, purchasePayload, extraParamsBundle); + } } else { - bundle = billingService.getBuyIntent(Constants.GOOGLE_API_VERSION, - contextPackageName, - productId, - purchaseType, - purchasePayload); + if(extraParamsBundle == null) // API v3 + { + bundle = billingService.getBuyIntent(Constants.GOOGLE_API_VERSION, + contextPackageName, + productId, + purchaseType, + purchasePayload); + + } + else // API v7+ + { + bundle = billingService.getBuyIntentExtraParams(Constants.GOOGLE_API_VR_SUPPORTED_VERSION, + contextPackageName, productId, purchaseType, purchasePayload, extraParamsBundle); + } } if (bundle != null) diff --git a/library/src/main/java/com/anjlab/android/iab/v3/Constants.java b/library/src/main/java/com/anjlab/android/iab/v3/Constants.java index 2c83eed2..3900a932 100644 --- a/library/src/main/java/com/anjlab/android/iab/v3/Constants.java +++ b/library/src/main/java/com/anjlab/android/iab/v3/Constants.java @@ -19,6 +19,7 @@ public class Constants { public static final int GOOGLE_API_VERSION = 3; public static final int GOOGLE_API_SUBSCRIPTION_CHANGE_VERSION = 5; + public static final int GOOGLE_API_VR_SUPPORTED_VERSION = 7; public static final String PRODUCT_TYPE_MANAGED = "inapp"; public static final String PRODUCT_TYPE_SUBSCRIPTION = "subs"; @@ -86,4 +87,7 @@ public class Constants public static final int BILLING_ERROR_CONSUME_FAILED = 111; public static final int BILLING_ERROR_SKUDETAILS_FAILED = 112; public static final int BILLING_ERROR_BIND_PLAY_STORE_FAILED = 113; + + public static final String EXTRA_PARAMS_KEY_VR = "vr"; + public static final String EXTRA_PARAMS_KEY_SKU_TO_REPLACE = "skusToReplace"; }