com.anjlab.android.iab.v3.BillingProcessor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of library Show documentation
Show all versions of library Show documentation
A lightweight implementation of Android In-app Billing Version 3
/**
* 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.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.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(String productId, TransactionDetails details);
void onPurchaseHistoryRestored();
void onBillingError(int errorCode, Throwable error);
void onBillingInitialized();
}
private static final Date DATE_MERCHANT_LIMIT_1 = new Date(2012, 12, 5); //5th December 2012
private static final Date DATE_MERCHANT_LIMIT_2 = new Date(2015, 7, 20); //21st July 2015
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 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();
}
};
public BillingProcessor(Context context, String licenseKey, IBillingHandler handler) {
this(context, licenseKey, null, handler);
}
public BillingProcessor(Context context, String licenseKey, String merchantId, IBillingHandler handler) {
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;
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);
if (eventHandler != null)
eventHandler.onBillingError(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) {
if (eventHandler != null)
eventHandler.onBillingError(Constants.BILLING_ERROR_FAILED_LOAD_PURCHASES, e);
Log.e(LOG_TAG, "Error in loadPurchasesByType", e);
}
return false;
}
public boolean loadOwnedPurchasesFromGoogle() {
return isInitialized() &&
loadPurchasesByType(Constants.PRODUCT_TYPE_MANAGED, cachedProducts) &&
loadPurchasesByType(Constants.PRODUCT_TYPE_SUBSCRIPTION, cachedSubscriptions);
}
public boolean purchase(Activity activity, String productId) {
return purchase(activity, productId, Constants.PRODUCT_TYPE_MANAGED, null);
}
public boolean subscribe(Activity activity, String productId) {
return purchase(activity, 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);
}
public boolean isOnTimePurchaseSupported(){
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;
}
/**
* 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) {
List oldProductIds = null;
if (!TextUtils.isEmpty(oldProductId)) {
oldProductIds = new ArrayList<>(1);
oldProductIds.add(oldProductId);
}
return updateSubscription(activity, oldProductIds, productId);
}
/**
* 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) {
if (oldProductIds != null && !isSubscriptionUpdateSupported())
return false;
return purchase(activity, oldProductIds, productId, Constants.PRODUCT_TYPE_SUBSCRIPTION, null);
}
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) {
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))
bundle = billingService.getBuyIntentToReplaceSkus(Constants.GOOGLE_API_SUBSCRIPTION_CHANGE_VERSION, contextPackageName, oldProductIds, productId, purchaseType, purchasePayload);
else
bundle = billingService.getBuyIntent(Constants.GOOGLE_API_VERSION, contextPackageName, productId, purchaseType, purchasePayload);
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 if (eventHandler != null)
eventHandler.onBillingError(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!");
if (eventHandler != null)
eventHandler.onBillingError(Constants.BILLING_ERROR_INVALID_MERCHANT_ID, null);
return false;
}
if (eventHandler != null) {
if (details == null)
details = getSubscriptionTransactionDetails(productId);
eventHandler.onProductPurchased(productId, details);
}
} else if (eventHandler != null)
eventHandler.onBillingError(Constants.BILLING_ERROR_FAILED_TO_INITIALIZE_PURCHASE, null);
}
return true;
} catch (Exception e) {
Log.e(LOG_TAG, "Error in purchase", e);
if (eventHandler != null)
eventHandler.onBillingError(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.purchaseTime.before(DATE_MERCHANT_LIMIT_1)) //new format [merchantId].[orderId] applied or not?
return true;
if (details.purchaseTime.after(DATE_MERCHANT_LIMIT_2)) //newest format applied
return true;
if (details.orderId == null || details.orderId.trim().length() == 0)
return false;
int index = details.orderId.indexOf('.');
if (index <= 0)
return false; //protect on missing merchant id
//extract merchant id
String merchantId = details.orderId.substring(0, index);
return merchantId.compareTo(developerMerchantId) == 0;
}
private TransactionDetails getPurchaseTransactionDetails(String productId, BillingCache cache) {
PurchaseInfo details = cache.getDetails(productId);
if (details != null && !TextUtils.isEmpty(details.responseData)) {
try {
return new TransactionDetails(details);
} catch (JSONException e) {
Log.e(LOG_TAG, "Failed to load saved purchase details for " + productId, e);
}
}
return null;
}
public boolean consumePurchase(String productId) {
if (!isInitialized())
return false;
try {
TransactionDetails transactionDetails = getPurchaseTransactionDetails(productId, cachedProducts);
if (transactionDetails != null && !TextUtils.isEmpty(transactionDetails.purchaseToken)) {
int response = billingService.consumePurchase(Constants.GOOGLE_API_VERSION, contextPackageName, transactionDetails.purchaseToken);
if (response == Constants.BILLING_RESPONSE_RESULT_OK) {
cachedProducts.remove(productId);
Log.d(LOG_TAG, "Successfully consumed " + productId + " purchase.");
return true;
} else {
if (eventHandler != null)
eventHandler.onBillingError(response, null);
Log.e(LOG_TAG, String.format("Failed to consume %s: error %d", productId, response));
}
}
} catch (Exception e) {
Log.e(LOG_TAG, "Error in consumePurchase", e);
if (eventHandler != null)
eventHandler.onBillingError(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 {
if (eventHandler != null)
eventHandler.onBillingError(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);
if (eventHandler != null)
eventHandler.onBillingError(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);
}
public TransactionDetails getPurchaseTransactionDetails(String productId) {
return getPurchaseTransactionDetails(productId, cachedProducts);
}
public TransactionDetails getSubscriptionTransactionDetails(String productId) {
return getPurchaseTransactionDetails(productId, cachedSubscriptions);
}
private String getDeveloperPayloadFromPurchaseData(JSONObject purchase) {
String value = null;
try
{
value = purchase.has(Constants.RESPONSE_PAYLOAD)
? purchase.getString(Constants.RESPONSE_PAYLOAD) : null;
}
catch (JSONException e)
{
Log.e(LOG_TAG, "Failed to extract developer payload value!");
}
return value != null ? value : "";
}
private boolean validateDeveloperPayload(String expectedValue, String actualValue) {
return expectedValue.equals(actualValue);
}
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));
String purchasePayload = getPurchasePayload();
if (resultCode == Activity.RESULT_OK && responseCode == Constants.BILLING_RESPONSE_RESULT_OK && !TextUtils.isEmpty(purchasePayload)) {
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);
String developerPayload = getDeveloperPayloadFromPurchaseData(purchase);
boolean purchasedSubscription = purchasePayload.startsWith(Constants.PRODUCT_TYPE_SUBSCRIPTION);
if (validateDeveloperPayload(purchasePayload, developerPayload)) {
if (verifyPurchaseSignature(productId, purchaseData, dataSignature)) {
BillingCache cache = purchasedSubscription ? 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!");
if (eventHandler != null)
eventHandler.onBillingError(Constants.BILLING_ERROR_INVALID_SIGNATURE, null);
}
} else {
Log.e(LOG_TAG, String.format("Payload mismatch: %s != %s", purchasePayload, developerPayload));
if (eventHandler != null)
eventHandler.onBillingError(Constants.BILLING_ERROR_INVALID_DEVELOPER_PAYLOAD, null);
}
} catch (Exception e) {
Log.e(LOG_TAG, "Error in handleActivityResult", e);
if (eventHandler != null)
eventHandler.onBillingError(Constants.BILLING_ERROR_OTHER_ERROR, e);
}
} else {
if (eventHandler != null)
eventHandler.onBillingError(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.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);
}
}