Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.anjlab.android.iab.v3.BillingProcessor Maven / Gradle / Ivy
/**
* Copyright 2014 AnjLab
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.anjlab.android.iab.v3;
import android.app.Activity;
import android.app.PendingIntent;
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.ResolveInfo;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import com.android.vending.billing.IInAppBillingService;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.UUID;
public class BillingProcessor extends BillingBase
{
/**
* Callback methods where billing events are reported.
* Apps must implement one of these to construct a BillingProcessor.
*/
public interface IBillingHandler
{
void onProductPurchased(@NonNull String productId, @Nullable TransactionDetails details);
void onPurchaseHistoryRestored();
void onBillingError(int errorCode, @Nullable Throwable error);
void onBillingInitialized();
}
private static final Date DATE_MERCHANT_LIMIT_1; //5th December 2012
private static final Date DATE_MERCHANT_LIMIT_2; //21st July 2015
static
{
Calendar calendar = Calendar.getInstance();
calendar.set(2012, Calendar.DECEMBER, 5);
DATE_MERCHANT_LIMIT_1 = calendar.getTime();
calendar.set(2015, Calendar.JULY, 21);
DATE_MERCHANT_LIMIT_2 = calendar.getTime();
}
private static final int PURCHASE_FLOW_REQUEST_CODE = 32459;
private static final String LOG_TAG = "iabv3";
private static final String SETTINGS_VERSION = ".v2_6";
private static final String RESTORE_KEY = ".products.restored" + SETTINGS_VERSION;
private static final String MANAGED_PRODUCTS_CACHE_KEY = ".products.cache" + SETTINGS_VERSION;
private static final String SUBSCRIPTIONS_CACHE_KEY = ".subscriptions.cache" + SETTINGS_VERSION;
private static final String PURCHASE_PAYLOAD_CACHE_KEY = ".purchase.last" + SETTINGS_VERSION;
private IInAppBillingService billingService;
private String contextPackageName;
private String signatureBase64;
private BillingCache cachedProducts;
private BillingCache cachedSubscriptions;
private IBillingHandler eventHandler;
private String developerMerchantId;
private boolean isOneTimePurchasesSupported;
private boolean isSubsUpdateSupported;
private boolean isSubscriptionExtraParamsSupported;
private boolean isOneTimePurchaseExtraParamsSupported;
private class HistoryInitializationTask extends AsyncTask
{
@Override
protected Boolean doInBackground(Void... nothing)
{
if (!isPurchaseHistoryRestored())
{
loadOwnedPurchasesFromGoogle();
return true;
}
return false;
}
@Override
protected void onPostExecute(Boolean restored)
{
if (restored)
{
setPurchaseHistoryRestored();
if (eventHandler != null)
{
eventHandler.onPurchaseHistoryRestored();
}
}
if (eventHandler != null)
{
eventHandler.onBillingInitialized();
}
}
}
private ServiceConnection serviceConnection = new ServiceConnection()
{
@Override
public void onServiceDisconnected(ComponentName name)
{
billingService = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service)
{
billingService = IInAppBillingService.Stub.asInterface(service);
new HistoryInitializationTask().execute();
}
};
/**
* Returns a new {@link BillingProcessor}, without immediately binding to Play Services. If you use
* this factory, then you must call {@link #initialize()} afterwards.
*/
public static BillingProcessor newBillingProcessor(Context context, String licenseKey, IBillingHandler handler)
{
return newBillingProcessor(context, licenseKey, null, handler);
}
/**
* Returns a new {@link BillingProcessor}, without immediately binding to Play Services. If you use
* this factory, then you must call {@link #initialize()} afterwards.
*/
public static BillingProcessor newBillingProcessor(Context context, String licenseKey, String merchantId,
IBillingHandler handler)
{
return new BillingProcessor(context, licenseKey, merchantId, handler, false);
}
public BillingProcessor(Context context, String licenseKey, IBillingHandler handler)
{
this(context, licenseKey, null, handler);
}
public BillingProcessor(Context context, String licenseKey, String merchantId,
IBillingHandler handler)
{
this(context, licenseKey, merchantId, handler, true);
}
private BillingProcessor(Context context, String licenseKey, String merchantId, IBillingHandler handler,
boolean bindImmediately)
{
super(context.getApplicationContext());
signatureBase64 = licenseKey;
eventHandler = handler;
contextPackageName = getContext().getPackageName();
cachedProducts = new BillingCache(getContext(), MANAGED_PRODUCTS_CACHE_KEY);
cachedSubscriptions = new BillingCache(getContext(), SUBSCRIPTIONS_CACHE_KEY);
developerMerchantId = merchantId;
if (bindImmediately)
{
bindPlayServices();
}
}
/**
* Binds to Play Services. When complete, caller will be notified via
* {@link IBillingHandler#onBillingInitialized()}.
*/
public void initialize()
{
bindPlayServices();
}
private static Intent getBindServiceIntent()
{
Intent intent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
intent.setPackage("com.android.vending");
return intent;
}
private void bindPlayServices()
{
try
{
getContext().bindService(getBindServiceIntent(), serviceConnection, Context.BIND_AUTO_CREATE);
}
catch (Exception e)
{
Log.e(LOG_TAG, "error in bindPlayServices", e);
reportBillingError(Constants.BILLING_ERROR_BIND_PLAY_STORE_FAILED, e);
}
}
public static boolean isIabServiceAvailable(Context context)
{
final PackageManager packageManager = context.getPackageManager();
List list = packageManager.queryIntentServices(getBindServiceIntent(), 0);
return list != null && list.size() > 0;
}
public void release()
{
if (isInitialized() && serviceConnection != null)
{
try
{
getContext().unbindService(serviceConnection);
}
catch (Exception e)
{
Log.e(LOG_TAG, "Error in release", e);
}
billingService = null;
}
}
public boolean isInitialized()
{
return billingService != null;
}
public boolean isPurchased(String productId)
{
return cachedProducts.includesProduct(productId);
}
public boolean isSubscribed(String productId)
{
return cachedSubscriptions.includesProduct(productId);
}
public List listOwnedProducts()
{
return cachedProducts.getContents();
}
public List listOwnedSubscriptions()
{
return cachedSubscriptions.getContents();
}
private boolean loadPurchasesByType(String type, BillingCache cacheStorage)
{
if (!isInitialized())
{
return false;
}
try
{
Bundle bundle = billingService.getPurchases(Constants.GOOGLE_API_VERSION,
contextPackageName, type, null);
if (bundle.getInt(Constants.RESPONSE_CODE) == Constants.BILLING_RESPONSE_RESULT_OK)
{
cacheStorage.clear();
ArrayList purchaseList =
bundle.getStringArrayList(Constants.INAPP_PURCHASE_DATA_LIST);
ArrayList signatureList =
bundle.getStringArrayList(Constants.INAPP_DATA_SIGNATURE_LIST);
if (purchaseList != null)
{
for (int i = 0; i < purchaseList.size(); i++)
{
String jsonData = purchaseList.get(i);
if (!TextUtils.isEmpty(jsonData))
{
JSONObject purchase = new JSONObject(jsonData);
String signature = signatureList != null && signatureList.size() >
i ? signatureList.get(i) : null;
cacheStorage.put(purchase.getString(Constants.RESPONSE_PRODUCT_ID),
jsonData,
signature);
}
}
}
return true;
}
}
catch (Exception e)
{
reportBillingError(Constants.BILLING_ERROR_FAILED_LOAD_PURCHASES, e);
Log.e(LOG_TAG, "Error in loadPurchasesByType", e);
}
return false;
}
/**
* Attempt to fetch purchases from the server and update our cache if successful
*
* @return {@code true} if all retrievals are successful, {@code false} otherwise
*/
public boolean loadOwnedPurchasesFromGoogle()
{
return loadPurchasesByType(Constants.PRODUCT_TYPE_MANAGED, cachedProducts) &&
loadPurchasesByType(Constants.PRODUCT_TYPE_SUBSCRIPTION, cachedSubscriptions);
}
public boolean purchase(Activity activity, String productId)
{
return purchase(activity, null, productId, Constants.PRODUCT_TYPE_MANAGED, null);
}
public boolean subscribe(Activity activity, String productId)
{
return purchase(activity, null, productId, Constants.PRODUCT_TYPE_SUBSCRIPTION, null);
}
public boolean purchase(Activity activity, String productId, String developerPayload)
{
return purchase(activity, productId, Constants.PRODUCT_TYPE_MANAGED, developerPayload);
}
public boolean subscribe(Activity activity, String productId, String developerPayload)
{
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 extraParams 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 extraParams A bundle object containing extra parameters to pass to getBuyIntentExtraParams()
* @return {@code false} if the billing system is not initialized, {@code productId} is empty or if an exception occurs.
* Will return {@code true} otherwise.
* @see extra
* params documentation on developer.android.com
*/
public boolean subscribe(Activity activity, String productId, String developerPayload, Bundle extraParams)
{
return purchase(activity,
null,
productId,
Constants.PRODUCT_TYPE_SUBSCRIPTION,
developerPayload,
isSubscriptionWithExtraParamsSupported(extraParams) ? extraParams : null);
}
public boolean isOneTimePurchaseSupported()
{
if(!isInitialized())
{
Log.e(LOG_TAG, "Make sure BillingProcessor was initialized before calling isOneTimePurchaseSupported()");
return false;
}
if (isOneTimePurchasesSupported)
{
return true;
}
try
{
int response = billingService.isBillingSupported(Constants.GOOGLE_API_VERSION,
contextPackageName,
Constants.PRODUCT_TYPE_MANAGED);
isOneTimePurchasesSupported = response == Constants.BILLING_RESPONSE_RESULT_OK;
}
catch (RemoteException e)
{
e.printStackTrace();
}
return isOneTimePurchasesSupported;
}
public boolean isSubscriptionUpdateSupported()
{
// Avoid calling the service again if this value is true
if (isSubsUpdateSupported)
{
return true;
}
try
{
int response =
billingService.isBillingSupported(Constants.GOOGLE_API_SUBSCRIPTION_CHANGE_VERSION,
contextPackageName,
Constants.PRODUCT_TYPE_SUBSCRIPTION);
isSubsUpdateSupported = response == Constants.BILLING_RESPONSE_RESULT_OK;
}
catch (RemoteException e)
{
e.printStackTrace();
}
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;
}
/**
* Checks if API supports version 6 which required to request purchase history
* @param type product type, accepts either {@value Constants#PRODUCT_TYPE_MANAGED}
* or {@value Constants#PRODUCT_TYPE_SUBSCRIPTION}
* @return {@code true} if feature supported {@code false} otherwise
*/
public boolean isRequestBillingHistorySupported(String type) throws BillingCommunicationException
{
if (!type.equals(Constants.PRODUCT_TYPE_MANAGED) && !type.equals(Constants.PRODUCT_TYPE_SUBSCRIPTION))
{
throw new RuntimeException("Unsupported type " + type);
}
IInAppBillingService billing = billingService;
if (billing != null)
{
try
{
int response = billing.isBillingSupported(Constants.GOOGLE_API_REQUEST_PURCHASE_HISTORY_VERSION,
contextPackageName, type);
return response == Constants.BILLING_RESPONSE_RESULT_OK;
}
catch (RemoteException e)
{
throw new BillingCommunicationException(e);
}
}
else
{
throw new BillingCommunicationException("Billing service isn't connected");
}
}
/**
* Change subscription i.e. upgrade or downgrade
*
* @param activity the activity calling this method
* @param oldProductId passing null or empty string will act the same as {@link #subscribe(Activity, String)}
* @param productId the new subscription id
* @return {@code false} if {@code oldProductId} is not {@code null} AND change subscription
* is not supported.
*/
public boolean updateSubscription(Activity activity, String oldProductId, String productId)
{
return updateSubscription(activity, oldProductId, productId, null);
}
/**
* Change subscription i.e. upgrade or downgrade
*
* @param activity the activity calling this method
* @param oldProductId passing null or empty string will act the same as {@link #subscribe(Activity, String)}
* @param productId the new subscription id
* @param developerPayload the developer payload
* @return {@code false} if {@code oldProductId} is not {@code null} AND change subscription
* is not supported.
*/
public boolean updateSubscription(Activity activity, String oldProductId, String productId, String developerPayload)
{
List oldProductIds = null;
if (!TextUtils.isEmpty(oldProductId))
{
oldProductIds = Collections.singletonList(oldProductId);
}
return updateSubscription(activity, oldProductIds, productId, developerPayload);
}
/**
* Change subscription i.e. upgrade or downgrade
*
* @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
* @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)
{
return updateSubscription(activity, oldProductIds, productId, null);
}
/**
* Change subscription i.e. upgrade or downgrade
*
* @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
* @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)
{
if (oldProductIds != null && !isSubscriptionUpdateSupported())
{
return false;
}
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()
* @return {@code false} if {@code oldProductIds} is not {@code null} AND change subscription
* is not supported.
* @see extra
* params documentation on developer.android.com
*/
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)
{
return purchase(activity, null, productId, purchaseType, developerPayload);
}
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))
{
return false;
}
try
{
String purchasePayload = purchaseType + ":" + productId;
if (!purchaseType.equals(Constants.PRODUCT_TYPE_SUBSCRIPTION))
{
purchasePayload += ":" + UUID.randomUUID().toString();
}
if (developerPayload != null)
{
purchasePayload += ":" + developerPayload;
}
savePurchasePayload(purchasePayload);
Bundle bundle;
if (oldProductIds != null && purchaseType.equals(Constants.PRODUCT_TYPE_SUBSCRIPTION))
{
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.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
{
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)
{
int response = bundle.getInt(Constants.RESPONSE_CODE);
if (response == Constants.BILLING_RESPONSE_RESULT_OK)
{
PendingIntent pendingIntent = bundle.getParcelable(Constants.BUY_INTENT);
if (activity != null && pendingIntent != null)
{
activity.startIntentSenderForResult(pendingIntent.getIntentSender(),
PURCHASE_FLOW_REQUEST_CODE,
new Intent(), 0, 0, 0);
}
else
{
reportBillingError(Constants.BILLING_ERROR_LOST_CONTEXT, null);
}
}
else if (response == Constants.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED)
{
if (!isPurchased(productId) && !isSubscribed(productId))
{
loadOwnedPurchasesFromGoogle();
}
TransactionDetails details = getPurchaseTransactionDetails(productId);
if (!checkMerchant(details))
{
Log.i(LOG_TAG, "Invalid or tampered merchant id!");
reportBillingError(Constants.BILLING_ERROR_INVALID_MERCHANT_ID, null);
return false;
}
if (eventHandler != null)
{
if (details == null)
{
details = getSubscriptionTransactionDetails(productId);
}
eventHandler.onProductPurchased(productId, details);
}
}
else
{
reportBillingError(Constants.BILLING_ERROR_FAILED_TO_INITIALIZE_PURCHASE, null);
}
}
return true;
}
catch (Exception e)
{
Log.e(LOG_TAG, "Error in purchase", e);
reportBillingError(Constants.BILLING_ERROR_OTHER_ERROR, e);
}
return false;
}
/**
* Checks merchant's id validity. If purchase was generated by Freedom alike program it doesn't know
* real merchant id, unless publisher GoogleId was hacked
* If merchantId was not supplied function checks nothing
*
* @param details TransactionDetails
* @return boolean
*/
private boolean checkMerchant(TransactionDetails details)
{
if (developerMerchantId == null) //omit merchant id checking
{
return true;
}
if (details.purchaseInfo.purchaseData.purchaseTime.before(DATE_MERCHANT_LIMIT_1)) //newest format applied
{
return true;
}
if (details.purchaseInfo.purchaseData.purchaseTime.after(DATE_MERCHANT_LIMIT_2)) //newest format applied
{
return true;
}
if (details.purchaseInfo.purchaseData.orderId == null ||
details.purchaseInfo.purchaseData.orderId.trim().length() == 0)
{
return false;
}
int index = details.purchaseInfo.purchaseData.orderId.indexOf('.');
if (index <= 0)
{
return false; //protect on missing merchant id
}
//extract merchant id
String merchantId = details.purchaseInfo.purchaseData.orderId.substring(0, index);
return merchantId.compareTo(developerMerchantId) == 0;
}
@Nullable
private TransactionDetails getPurchaseTransactionDetails(String productId, BillingCache cache)
{
PurchaseInfo details = cache.getDetails(productId);
if (details != null && !TextUtils.isEmpty(details.responseData))
{
return new TransactionDetails(details);
}
return null;
}
public boolean consumePurchase(String productId)
{
if (!isInitialized())
{
return false;
}
try
{
TransactionDetails transaction = getPurchaseTransactionDetails(productId, cachedProducts);
if (transaction != null && !TextUtils.isEmpty(transaction.purchaseInfo.purchaseData.purchaseToken))
{
int response = billingService.consumePurchase(Constants.GOOGLE_API_VERSION,
contextPackageName,
transaction.purchaseInfo.purchaseData.purchaseToken);
if (response == Constants.BILLING_RESPONSE_RESULT_OK)
{
cachedProducts.remove(productId);
Log.d(LOG_TAG, "Successfully consumed " + productId + " purchase.");
return true;
}
else
{
reportBillingError(response, null);
Log.e(LOG_TAG, String.format("Failed to consume %s: %d", productId, response));
}
}
}
catch (Exception e)
{
Log.e(LOG_TAG, "Error in consumePurchase", e);
reportBillingError(Constants.BILLING_ERROR_CONSUME_FAILED, e);
}
return false;
}
private SkuDetails getSkuDetails(String productId, String purchaseType)
{
ArrayList productIdList = new ArrayList();
productIdList.add(productId);
List skuDetailsList = getSkuDetails(productIdList, purchaseType);
if (skuDetailsList != null && skuDetailsList.size() > 0)
{
return skuDetailsList.get(0);
}
return null;
}
private List getSkuDetails(ArrayList productIdList, String purchaseType)
{
if (billingService != null && productIdList != null && productIdList.size() > 0)
{
try
{
Bundle products = new Bundle();
products.putStringArrayList(Constants.PRODUCTS_LIST, productIdList);
Bundle skuDetails = billingService.getSkuDetails(Constants.GOOGLE_API_VERSION,
contextPackageName,
purchaseType,
products);
int response = skuDetails.getInt(Constants.RESPONSE_CODE);
if (response == Constants.BILLING_RESPONSE_RESULT_OK)
{
ArrayList productDetails = new ArrayList();
List detailsList = skuDetails.getStringArrayList(Constants.DETAILS_LIST);
if (detailsList != null)
{
for (String responseLine : detailsList)
{
JSONObject object = new JSONObject(responseLine);
SkuDetails product = new SkuDetails(object);
productDetails.add(product);
}
}
return productDetails;
}
else
{
reportBillingError(response, null);
Log.e(LOG_TAG, String.format("Failed to retrieve info for %d products, %d",
productIdList.size(),
response));
}
}
catch (Exception e)
{
Log.e(LOG_TAG, "Failed to call getSkuDetails", e);
reportBillingError(Constants.BILLING_ERROR_SKUDETAILS_FAILED, e);
}
}
return null;
}
public SkuDetails getPurchaseListingDetails(String productId)
{
return getSkuDetails(productId, Constants.PRODUCT_TYPE_MANAGED);
}
public SkuDetails getSubscriptionListingDetails(String productId)
{
return getSkuDetails(productId, Constants.PRODUCT_TYPE_SUBSCRIPTION);
}
public List getPurchaseListingDetails(ArrayList productIdList)
{
return getSkuDetails(productIdList, Constants.PRODUCT_TYPE_MANAGED);
}
public List getSubscriptionListingDetails(ArrayList productIdList)
{
return getSkuDetails(productIdList, Constants.PRODUCT_TYPE_SUBSCRIPTION);
}
@Nullable
public TransactionDetails getPurchaseTransactionDetails(String productId)
{
return getPurchaseTransactionDetails(productId, cachedProducts);
}
@Nullable
public TransactionDetails getSubscriptionTransactionDetails(String productId)
{
return getPurchaseTransactionDetails(productId, cachedSubscriptions);
}
private String detectPurchaseTypeFromPurchaseResponseData(JSONObject purchase)
{
String purchasePayload = getPurchasePayload();
// regular flow, based on developer payload
if (!TextUtils.isEmpty(purchasePayload) && purchasePayload.startsWith(Constants.PRODUCT_TYPE_SUBSCRIPTION))
{
return Constants.PRODUCT_TYPE_SUBSCRIPTION;
}
// backup check for the promo codes (no payload available)
if (purchase != null && purchase.has(Constants.RESPONSE_AUTO_RENEWING))
{
return Constants.PRODUCT_TYPE_SUBSCRIPTION;
}
return Constants.PRODUCT_TYPE_MANAGED;
}
public boolean handleActivityResult(int requestCode, int resultCode, Intent data)
{
if (requestCode != PURCHASE_FLOW_REQUEST_CODE)
{
return false;
}
if (data == null)
{
Log.e(LOG_TAG, "handleActivityResult: data is null!");
return false;
}
int responseCode = data.getIntExtra(Constants.RESPONSE_CODE, Constants.BILLING_RESPONSE_RESULT_OK);
Log.d(LOG_TAG, String.format("resultCode = %d, responseCode = %d", resultCode, responseCode));
if (resultCode == Activity.RESULT_OK &&
responseCode == Constants.BILLING_RESPONSE_RESULT_OK)
{
String purchaseData = data.getStringExtra(Constants.INAPP_PURCHASE_DATA);
String dataSignature = data.getStringExtra(Constants.RESPONSE_INAPP_SIGNATURE);
try
{
JSONObject purchase = new JSONObject(purchaseData);
String productId = purchase.getString(Constants.RESPONSE_PRODUCT_ID);
if (verifyPurchaseSignature(productId, purchaseData, dataSignature))
{
String purchaseType = detectPurchaseTypeFromPurchaseResponseData(purchase);
BillingCache cache = purchaseType.equals(Constants.PRODUCT_TYPE_SUBSCRIPTION)
? cachedSubscriptions : cachedProducts;
cache.put(productId, purchaseData, dataSignature);
if (eventHandler != null)
{
eventHandler.onProductPurchased(
productId,
new TransactionDetails(new PurchaseInfo(purchaseData, dataSignature)));
}
}
else
{
Log.e(LOG_TAG, "Public key signature doesn't match!");
reportBillingError(Constants.BILLING_ERROR_INVALID_SIGNATURE, null);
}
}
catch (Exception e)
{
Log.e(LOG_TAG, "Error in handleActivityResult", e);
reportBillingError(Constants.BILLING_ERROR_OTHER_ERROR, e);
}
savePurchasePayload(null);
}
else
{
reportBillingError(responseCode, null);
}
return true;
}
private boolean verifyPurchaseSignature(String productId, String purchaseData, String dataSignature)
{
try
{
/*
* Skip the signature check if the provided License Key is NULL and return true in order to
* continue the purchase flow
*/
return TextUtils.isEmpty(signatureBase64) ||
Security.verifyPurchase(productId, signatureBase64, purchaseData, dataSignature);
}
catch (Exception e)
{
return false;
}
}
public boolean isValidTransactionDetails(TransactionDetails transactionDetails)
{
return verifyPurchaseSignature(transactionDetails.purchaseInfo.purchaseData.productId,
transactionDetails.purchaseInfo.responseData,
transactionDetails.purchaseInfo.signature) &&
checkMerchant(transactionDetails);
}
private boolean isPurchaseHistoryRestored()
{
return loadBoolean(getPreferencesBaseKey() + RESTORE_KEY, false);
}
private void setPurchaseHistoryRestored()
{
saveBoolean(getPreferencesBaseKey() + RESTORE_KEY, true);
}
private void savePurchasePayload(String value)
{
saveString(getPreferencesBaseKey() + PURCHASE_PAYLOAD_CACHE_KEY, value);
}
private String getPurchasePayload()
{
return loadString(getPreferencesBaseKey() + PURCHASE_PAYLOAD_CACHE_KEY, null);
}
private void reportBillingError(int errorCode, Throwable error)
{
if (eventHandler != null)
{
eventHandler.onBillingError(errorCode, error);
}
}
/**
* Returns the most recent purchase made by the user for each SKU, even if that purchase is expired, canceled, or consumed.
*
* @param type product type, accepts either {@value Constants#PRODUCT_TYPE_MANAGED} or
* {@value Constants#PRODUCT_TYPE_SUBSCRIPTION}
* @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 @NotNull list of billing history records
* @throws BillingCommunicationException if billing isn't connected or there was an error during request execution
*/
public List getPurchaseHistory(String type, Bundle extraParams) throws BillingCommunicationException
{
if (!type.equals(Constants.PRODUCT_TYPE_MANAGED) && !type.equals(Constants.PRODUCT_TYPE_SUBSCRIPTION))
{
throw new RuntimeException("Unsupported type " + type);
}
IInAppBillingService billing = billingService;
if (billing != null)
{
try
{
List result = new ArrayList<>();
int resultCode;
String continuationToken = null;
do
{
Bundle resultBundle = billing.getPurchaseHistory(Constants.GOOGLE_API_REQUEST_PURCHASE_HISTORY_VERSION,
contextPackageName, type, continuationToken, extraParams);
resultCode = resultBundle.getInt(Constants.RESPONSE_CODE);
if (resultCode == Constants.BILLING_RESPONSE_RESULT_OK)
{
List purchaseData = resultBundle.getStringArrayList(Constants.INAPP_PURCHASE_DATA_LIST);
List signatures = resultBundle.getStringArrayList(Constants.INAPP_DATA_SIGNATURE_LIST);
if (purchaseData != null && signatures != null)
{
for (int i = 0, max = purchaseData.size(); i < max; i++)
{
String data = purchaseData.get(i);
String signature = signatures.get(i);
BillingHistoryRecord record = new BillingHistoryRecord(data, signature);
result.add(record);
}
continuationToken = resultBundle.getString(Constants.INAPP_CONTINUATION_TOKEN);
}
}
} while (continuationToken != null && resultCode == Constants.BILLING_RESPONSE_RESULT_OK);
return result;
} catch (RemoteException | JSONException e)
{
throw new BillingCommunicationException(e);
}
}
else
{
throw new BillingCommunicationException("Billing service isn't connected");
}
}
}