com.treasuredata.android.billing.internal.BillingDelegate Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of td-android-sdk Show documentation
Show all versions of td-android-sdk Show documentation
Android SDK for Treasure Data Cloud
package com.treasuredata.android.billing.internal;
import android.content.Context;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class BillingDelegate {
private static final String TAG = BillingDelegate.class.getSimpleName();
// Method and class cache
private static final HashMap methodMap =
new HashMap<>();
private static final HashMap> classMap =
new HashMap<>();
// In App Billing Service class names
private static final String IN_APP_BILLING_SERVICE_STUB =
"com.android.vending.billing.IInAppBillingService$Stub";
private static final String IN_APP_BILLING_SERVICE =
"com.android.vending.billing.IInAppBillingService";
// In App Billing Service method names
private static final String AS_INTERFACE = "asInterface";
private static final String GET_SKU_DETAILS = "getSkuDetails";
private static final String GET_PURCHASES = "getPurchases";
private static final String GET_PURCHASE_HISTORY = "getPurchaseHistory";
private static final String IS_BILLING_SUPPORTED = "isBillingSupported";
// In App Purchase key
private static final String RESPONSE_CODE = "RESPONSE_CODE";
private static final String ITEM_ID_LIST = "ITEM_ID_LIST";
private static final String DETAILS_LIST = "DETAILS_LIST";
private static final String INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
private static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
private static final int MAX_QUERY_PURCHASE_NUM = 30;
private static final int PURCHASE_STOP_QUERY_TIME_SEC = 30 * 60; // 30 minutes
// Billing response codes
public static final int BILLING_RESPONSE_RESULT_OK = 0;
private BillingDelegate() {
}
public static boolean hasBillingService()
{
try {
Class.forName(IN_APP_BILLING_SERVICE_STUB);
return true;
} catch (ClassNotFoundException ignored) {
return false;
}
}
public static Object asInterface(Context context, IBinder service) {
Object[] args = new Object[]{service};
return invokeMethod(context, IN_APP_BILLING_SERVICE_STUB,
AS_INTERFACE, null, args);
}
public static boolean isBillingSupported(Context context,
Object inAppBillingObj, String type) {
if (inAppBillingObj == null) {
return false;
}
Object[] args = new Object[]{3, context.getApplicationContext().getPackageName(), type};
Object result = invokeMethod(context, IS_BILLING_SUPPORTED, inAppBillingObj, args);
return result != null && ((int) result) == 0;
}
/**
* Returns the current SKUs owned by the user of the type and package name specified along with
* purchase information and a signature of the data to be validated.
* This will return all SKUs that have been purchased that have not been consumed.
*/
public static List getPurchases(Context context, Object inAppBillingObj, String type) {
List purchases = new ArrayList<>();
if (!isBillingSupported(context, inAppBillingObj, type)) {
return purchases;
}
String continuationToken = null;
int queriedPurchaseCount = 0;
do {
Object[] args = new Object[]{3, context.getApplicationContext().getPackageName(), type, continuationToken};
Object resultObject = invokeMethod(context, GET_PURCHASES, inAppBillingObj, args);
continuationToken = null;
if (resultObject != null) {
Bundle purchaseBundle = (Bundle) resultObject;
int response = purchaseBundle.getInt(RESPONSE_CODE);
if (response == BILLING_RESPONSE_RESULT_OK) {
ArrayList purchaseDataList =
purchaseBundle.getStringArrayList(INAPP_PURCHASE_DATA_LIST);
if (purchaseDataList == null || purchaseDataList.isEmpty()) {
break;
}
queriedPurchaseCount += purchaseDataList.size();
purchases.addAll(purchaseDataList);
continuationToken = purchaseBundle.getString(INAPP_CONTINUATION_TOKEN);
}
}
} while (queriedPurchaseCount < MAX_QUERY_PURCHASE_NUM
&& continuationToken != null);
return purchases;
}
/**
* Returns the most recent purchase made by the user for each SKU, even if that purchase is
* expired, canceled, or consumed.
*/
public static List getPurchaseHistory(Context context, Object inAppBillingObj, String type) {
List purchases = new ArrayList<>();
if (!isBillingSupported(context, inAppBillingObj, type)) {
return purchases;
}
String continuationToken = null;
int queriedPurchaseCount = 0;
boolean reachTimeLimit = false;
do {
Object[] args = new Object[]{
6, context.getApplicationContext().getPackageName(), type, continuationToken, new Bundle()};
continuationToken = null;
Object resultObject = invokeMethod(context, IN_APP_BILLING_SERVICE,
GET_PURCHASE_HISTORY, inAppBillingObj, args);
if (resultObject == null) {
break;
}
long nowSec = System.currentTimeMillis() / 1000L;
Bundle purchaseBundle = (Bundle) resultObject;
int response = purchaseBundle.getInt(RESPONSE_CODE);
if (response == BILLING_RESPONSE_RESULT_OK) {
List purchaseDataList =
purchaseBundle.getStringArrayList(INAPP_PURCHASE_DATA_LIST);
for (String purchaseData : purchaseDataList) {
try {
JSONObject purchaseJSON = new JSONObject(purchaseData);
long purchaseTimeSec =
purchaseJSON.getLong("purchaseTime") / 1000L;
if (nowSec - purchaseTimeSec > PURCHASE_STOP_QUERY_TIME_SEC) {
reachTimeLimit = true;
break;
} else {
purchases.add(purchaseData);
queriedPurchaseCount++;
}
} catch (JSONException e) {
Log.e(TAG, "Unable to parse purchase, not a json object: ", e);
}
}
continuationToken = purchaseBundle.getString(INAPP_CONTINUATION_TOKEN);
}
} while (queriedPurchaseCount < MAX_QUERY_PURCHASE_NUM
&& continuationToken != null
&& !reachTimeLimit);
return purchases;
}
/**
* Provides details of a list of SKUs
* Given a list of SKUs of a valid type, this returns a map with key is each SKU id
* and value is JSON string containing the productId, price, title and description.
*/
public static Map getSkuDetails(
Context context, Object inAppBillingObj, ArrayList skuList, String type) {
Map skuDetailsMap = new HashMap<>();
if (skuList.isEmpty()) {
return skuDetailsMap;
}
if (!isBillingSupported(context, inAppBillingObj, type)) {
return skuDetailsMap;
}
Bundle querySkus = new Bundle();
querySkus.putStringArrayList(ITEM_ID_LIST, skuList);
Object[] args = new Object[]{
3, context.getApplicationContext().getPackageName(), type, querySkus};
Object result = invokeMethod(context, IN_APP_BILLING_SERVICE,
GET_SKU_DETAILS, inAppBillingObj, args);
if (result != null) {
Bundle bundle = (Bundle) result;
int response = bundle.getInt(RESPONSE_CODE);
if (response == BILLING_RESPONSE_RESULT_OK) {
List skuDetailsList = bundle.getStringArrayList(DETAILS_LIST);
if (skuDetailsList != null && skuList.size() == skuDetailsList.size()) {
for (int i = 0; i < skuList.size(); i++) {
skuDetailsMap.put(skuList.get(i), skuDetailsList.get(i));
}
}
}
}
return skuDetailsMap;
}
private static Object invokeMethod(Context context, String methodName, Object obj, Object[] args) {
return invokeMethod(context, IN_APP_BILLING_SERVICE, methodName, obj, args);
}
private static Object invokeMethod(Context context, String className, String methodName, Object obj, Object[] args) {
Class> classObj = getClass(context, className);
if (classObj == null) {
return null;
}
Method methodObj = getMethod(classObj, methodName);
if (methodObj == null) {
return null;
}
if (obj != null) {
obj = classObj.cast(obj);
}
try {
return methodObj.invoke(obj, args);
} catch (IllegalAccessException e) {
Log.e(TAG,
"Illegal access to method "
+ classObj.getName() + "." + methodObj.getName(), e);
} catch (InvocationTargetException e) {
Log.e(TAG,
"Invocation target exception in "
+ classObj.getName() + "." + methodObj.getName(), e);
}
return null;
}
private static Method getMethod(Class> classObj, String methodName) {
Method method = methodMap.get(methodName);
if (method != null) {
return method;
}
try {
Class>[] paramTypes = null;
switch (methodName) {
case AS_INTERFACE:
paramTypes = new Class[]{IBinder.class};
break;
case GET_SKU_DETAILS:
paramTypes = new Class[]{
Integer.TYPE, String.class, String.class, Bundle.class};
break;
case IS_BILLING_SUPPORTED:
paramTypes = new Class[]{
Integer.TYPE, String.class, String.class};
break;
case GET_PURCHASES:
paramTypes = new Class[]{
Integer.TYPE, String.class, String.class, String.class};
break;
case GET_PURCHASE_HISTORY:
paramTypes = new Class[]{
Integer.TYPE, String.class, String.class, String.class, Bundle.class};
break;
}
method = classObj.getDeclaredMethod(methodName, paramTypes);
methodMap.put(methodName, method);
} catch (NoSuchMethodException e) {
Log.e(TAG, classObj.getName() + "." + methodName + " method is not available", e);
}
return method;
}
private static Class> getClass(Context context, String className) {
Class> classObj = classMap.get(className);
if (classObj != null) {
return classObj;
}
try {
classObj = context.getClassLoader().loadClass(className);
classMap.put(className, classObj);
} catch (ClassNotFoundException e) {
Log.e(TAG, className + " is not available, please add "
+ className + " to the project's dependencies.", e);
}
return classObj;
}
}