/**
* 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.os.Bundle;
import android.os.IBinder;
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.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 int PURCHASE_FLOW_REQUEST_CODE = 2061984;
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 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);
if (!isPurchaseHistoryRestored() && loadOwnedPurchasesFromGoogle()) {
setPurchaseHistoryRestored();
if (eventHandler != null)
eventHandler.onPurchaseHistoryRestored();
}
if (eventHandler != null)
eventHandler.onBillingInitialized();
}
};
public BillingProcessor(Context context, String licenseKey, IBillingHandler handler) {
super(context);
signatureBase64 = licenseKey;
eventHandler = handler;
contextPackageName = context.getApplicationContext().getPackageName();
cachedProducts = new BillingCache(context, MANAGED_PRODUCTS_CACHE_KEY);
cachedSubscriptions = new BillingCache(context, SUBSCRIPTIONS_CACHE_KEY);
bindPlayServices();
}
private void bindPlayServices() {
try {
Intent iapIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
iapIntent.setPackage("com.android.vending");
getContext().bindService(iapIntent, serviceConnection, Context.BIND_AUTO_CREATE);
} catch (Exception e) {
Log.e(LOG_TAG, e.toString());
}
}
@Override
public void release() {
if (serviceConnection != null && getContext() != null) {
try {
getContext().unbindService(serviceConnection);
} catch (Exception e) {
Log.e(LOG_TAG, e.toString());
}
billingService = null;
}
cachedProducts.release();
super.release();
}
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<String> listOwnedProducts() {
return cachedProducts.getContents();
}
public List<String> 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<String> purchaseList = bundle.getStringArrayList(Constants.INAPP_PURCHASE_DATA_LIST);
ArrayList<String> signatureList = bundle.getStringArrayList(Constants.INAPP_DATA_SIGNATURE_LIST);
for (int i = 0; i < purchaseList.size(); i++) {
String jsonData = purchaseList.get(i);
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, e.toString());
}
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);
}
public boolean subscribe(Activity activity, String productId) {
return purchase(activity, productId, Constants.PRODUCT_TYPE_SUBSCRIPTION);
}
private boolean purchase(Activity activity, String productId, String purchaseType) {
if (!isInitialized() || TextUtils.isEmpty(productId) || TextUtils.isEmpty(purchaseType))
return false;
try {
String purchasePayload = purchaseType + ":" + UUID.randomUUID().toString();
savePurchasePayload(purchasePayload);
Bundle 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)
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();
if (eventHandler != null) {
TransactionDetails details = getPurchaseTransactionDetails(productId);
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, e.toString());
}
return false;
}
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);
}
}
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, e.toString());
}
return false;
}
private SkuDetails getSkuDetails(String productId, String purchaseType) {
if (billingService != null) {
try {
ArrayList<String> skuList = new ArrayList<String>();
skuList.add(productId);
Bundle products = new Bundle();
products.putStringArrayList(Constants.PRODUCTS_LIST, skuList);
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) {
for (String responseLine : skuDetails.getStringArrayList(Constants.DETAILS_LIST)) {
JSONObject object = new JSONObject(responseLine);
String responseProductId = object.getString(Constants.RESPONSE_PRODUCT_ID);
if (productId.equals(responseProductId))
return new SkuDetails(object);
}
} else {
if (eventHandler != null)
eventHandler.onBillingError(response, null);
Log.e(LOG_TAG, String.format("Failed to retrieve info for %s: error %d", productId, response));
}
} catch (Exception e) {
Log.e(LOG_TAG, String.format("Failed to call getSkuDetails %s", e.toString()));
}
}
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 TransactionDetails getPurchaseTransactionDetails(String productId) {
return getPurchaseTransactionDetails(productId, cachedProducts);
}
public TransactionDetails getSubscriptionTransactionDetails(String productId) {
return getPurchaseTransactionDetails(productId, cachedSubscriptions);
}
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);
if (Constants.DEBUG) {
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 = purchase.getString(Constants.RESPONSE_PAYLOAD);
if (developerPayload == null)
developerPayload = "";
boolean purchasedSubscription = purchasePayload.startsWith(Constants.PRODUCT_TYPE_SUBSCRIPTION);
if (purchasePayload.equals(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_SIGNATURE, null);
}
} catch (Exception e) {
Log.e(LOG_TAG, e.toString());
if (eventHandler != null)
eventHandler.onBillingError(Constants.BILLING_ERROR_OTHER_ERROR, null);
}
} 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
*/
if (TextUtils.isEmpty(signatureBase64)) {
return true;
} else {
return Security.verifyPurchase(productId, signatureBase64, purchaseData, dataSignature);
}
} catch (Exception e) {
return false;
}
}
public boolean isValid(TransactionDetails transactionDetails){
return verifyPurchaseSignature(transactionDetails.productId,
transactionDetails.purchaseInfo.responseData,transactionDetails.purchaseInfo.signature);
}
private boolean isPurchaseHistoryRestored() {
return loadBoolean(getPreferencesBaseKey() + RESTORE_KEY, false);
}
public 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);
}
}