2020-02-10 08:25:48 +01:00

497 lines
20 KiB
Java

package com.defold.iap;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;
import com.android.vending.billing.IInAppBillingService;
public class IapGooglePlay implements Handler.Callback {
public static final String PARAM_PRODUCT = "product";
public static final String PARAM_PRODUCT_TYPE = "product_type";
public static final String PARAM_PURCHASE_DATA = "purchase_data";
public static final String PARAM_AUTOFINISH_TRANSACTIONS = "auto_finish_transactions";
public static final String PARAM_MESSENGER = "com.defold.iap.messenger";
public static final String RESPONSE_CODE = "RESPONSE_CODE";
public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
public static enum Action {
BUY,
RESTORE,
PROCESS_PENDING_CONSUMABLES,
FINISH_TRANSACTION
}
public static final String TAG = "iap";
private Activity activity;
private Handler handler;
private Messenger messenger;
private ServiceConnection serviceConn;
private IInAppBillingService service;
private SkuDetailsThread skuDetailsThread;
private BlockingQueue<SkuRequest> skuRequestQueue = new ArrayBlockingQueue<SkuRequest>(16);
private IPurchaseListener purchaseListener;
private boolean initialized;
private boolean autoFinishTransactions;
private static interface ISkuRequestListener {
public void onProducts(int resultCode, JSONObject products);
}
private static class SkuRequest {
private ArrayList<String> skuList;
private ISkuRequestListener listener;
public SkuRequest(ArrayList<String> skuList, ISkuRequestListener listener) {
this.skuList = skuList;
this.listener = listener;
}
}
private class SkuDetailsThread extends Thread {
public boolean stop = false;
private void addProductsFromBundle(Bundle skuDetails, JSONObject products) throws JSONException {
int response = skuDetails.getInt("RESPONSE_CODE");
if (response == IapJNI.BILLING_RESPONSE_RESULT_OK) {
ArrayList<String> responseList = skuDetails.getStringArrayList("DETAILS_LIST");
for (String r : responseList) {
JSONObject product = new JSONObject(r);
products.put(product.getString("productId"), product);
}
}
else {
Log.e(TAG, "Failed to fetch product list: " + response);
}
}
@Override
public void run() {
while (!stop) {
try {
SkuRequest sr = skuRequestQueue.take();
if (service == null) {
Log.wtf(TAG, "service is null");
sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
continue;
}
if (activity == null) {
Log.wtf(TAG, "activity is null");
sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
continue;
}
String packageName = activity.getPackageName();
if (packageName == null)
{
Log.wtf(TAG, "activity packageName is null");
sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
continue;
}
try {
Bundle querySkus = new Bundle();
querySkus.putStringArrayList("ITEM_ID_LIST", sr.skuList);
JSONObject products = new JSONObject();
Bundle inappSkuDetails = service.getSkuDetails(3, packageName, "inapp", querySkus);
addProductsFromBundle(inappSkuDetails, products);
Bundle subscriptionSkuDetails = service.getSkuDetails(3, packageName, "subs", querySkus);
addProductsFromBundle(subscriptionSkuDetails, products);
sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_OK, products);
} catch (RemoteException e) {
Log.e(TAG, "Failed to fetch product list", e);
sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
} catch (JSONException e) {
Log.e(TAG, "Failed to fetch product list", e);
sr.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
}
} catch (InterruptedException e) {
continue;
}
}
}
}
public IapGooglePlay(Activity activity, boolean autoFinishTransactions) {
this.activity = activity;
this.autoFinishTransactions = autoFinishTransactions;
}
private void init() {
// NOTE: We must create Handler lazily as construction of
// handlers must be in the context of a "looper" on Android
if (this.initialized)
return;
this.initialized = true;
this.handler = new Handler(this);
this.messenger = new Messenger(this.handler);
serviceConn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
Log.v(TAG, "IAP disconnected");
service = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder binderService) {
Log.v(TAG, "IAP connected");
service = IInAppBillingService.Stub.asInterface(binderService);
skuDetailsThread = new SkuDetailsThread();
skuDetailsThread.start();
}
};
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
// Limit intent to vending package
serviceIntent.setPackage("com.android.vending");
List<ResolveInfo> intentServices = activity.getPackageManager().queryIntentServices(serviceIntent, 0);
if (intentServices != null && !intentServices.isEmpty()) {
// service available to handle that Intent
activity.bindService(serviceIntent, serviceConn, Context.BIND_AUTO_CREATE);
} else {
serviceConn = null;
Log.e(TAG, "Billing service unavailable on device.");
}
}
public void stop() {
this.activity.runOnUiThread(new Runnable() {
@Override
public void run() {
if (serviceConn != null) {
activity.unbindService(serviceConn);
serviceConn = null;
}
if (skuDetailsThread != null) {
skuDetailsThread.stop = true;
skuDetailsThread.interrupt();
try {
skuDetailsThread.join();
} catch (InterruptedException e) {
Log.wtf(TAG, "Failed to join thread", e);
}
}
}
});
}
private void queueSkuRequest(final SkuRequest request) {
this.activity.runOnUiThread(new Runnable() {
@Override
public void run() {
init();
if (serviceConn != null) {
try {
skuRequestQueue.put(request);
} catch (InterruptedException e) {
Log.wtf(TAG, "Failed to add sku request", e);
request.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, null);
}
} else {
request.listener.onProducts(IapJNI.BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, null);
}
}
});
}
public void listItems(final String skus, final IListProductsListener listener) {
ArrayList<String> skuList = new ArrayList<String>();
for (String x : skus.split(",")) {
if (x.trim().length() > 0) {
skuList.add(x);
}
}
queueSkuRequest(new SkuRequest(skuList, new ISkuRequestListener() {
@Override
public void onProducts(int resultCode, JSONObject products) {
if (products != null && products.length() > 0) {
try {
// go through all of the products and convert them into
// the generic product format used for all IAP implementations
Iterator<String> keys = products.keys();
while(keys.hasNext()) {
String key = keys.next();
if (products.get(key) instanceof JSONObject ) {
JSONObject product = products.getJSONObject(key);
products.put(key, convertProduct(product));
}
}
listener.onProductsResult(resultCode, products.toString());
}
catch(JSONException e) {
Log.wtf(TAG, "Failed to convert products", e);
listener.onProductsResult(resultCode, null);
}
}
else {
listener.onProductsResult(resultCode, null);
}
}
}));
}
// Convert the product data into the generic format shared between all Defold IAP implementations
private static JSONObject convertProduct(JSONObject product) {
try {
// Deep copy and modify
JSONObject p = new JSONObject(product.toString());
p.put("price_string", p.get("price"));
p.put("ident", p.get("productId"));
// It is not yet possible to obtain the price (num) and currency code on Android for the correct locale/region.
// They have a currency code (price_currency_code), which reflects the merchant's locale, instead of the user's
// https://code.google.com/p/marketbilling/issues/detail?id=93&q=currency%20code&colspec=ID%20Type%20Status%20Google%20Priority%20Milestone%20Owner%20Summary
double price = 0.0;
if (p.has("price_amount_micros")) {
price = p.getLong("price_amount_micros") * 0.000001;
}
String currency_code = "Unknown";
if (p.has("price_currency_code")) {
currency_code = (String)p.get("price_currency_code");
}
p.put("currency_code", currency_code);
p.put("price", price);
p.remove("productId");
p.remove("type");
p.remove("price_amount_micros");
p.remove("price_currency_code");
return p;
} catch (JSONException e) {
Log.wtf(TAG, "Failed to convert product json", e);
}
return null;
}
private void buyProduct(final String product, final String type, final IPurchaseListener listener) {
this.activity.runOnUiThread(new Runnable() {
@Override
public void run() {
init();
IapGooglePlay.this.purchaseListener = listener;
Intent intent = new Intent(activity, IapGooglePlayActivity.class);
intent.putExtra(PARAM_MESSENGER, messenger);
intent.putExtra(PARAM_AUTOFINISH_TRANSACTIONS, IapGooglePlay.this.autoFinishTransactions);
intent.putExtra(PARAM_PRODUCT, product);
intent.putExtra(PARAM_PRODUCT_TYPE, type);
intent.setAction(Action.BUY.toString());
activity.startActivity(intent);
}
});
}
public void buy(final String product, final IPurchaseListener listener) {
ArrayList<String> skuList = new ArrayList<String>();
skuList.add(product);
queueSkuRequest(new SkuRequest(skuList, new ISkuRequestListener() {
@Override
public void onProducts(int resultCode, JSONObject products) {
String type = "inapp";
if (resultCode == IapJNI.BILLING_RESPONSE_RESULT_OK && products != null) {
try {
JSONObject productData = products.getJSONObject(product);
type = productData.getString("type");
}
catch(JSONException e) {
Log.wtf(TAG, "Failed to get product type before buying, assuming type 'inapp'", e);
}
}
else {
Log.wtf(TAG, "Failed to list product before buying, assuming type 'inapp'");
}
buyProduct(product, type, listener);
}
}));
}
public void finishTransaction(final String receipt, final IPurchaseListener listener) {
if(IapGooglePlay.this.autoFinishTransactions) {
return;
}
this.activity.runOnUiThread(new Runnable() {
@Override
public void run() {
init();
IapGooglePlay.this.purchaseListener = listener;
Intent intent = new Intent(activity, IapGooglePlayActivity.class);
intent.putExtra(PARAM_MESSENGER, messenger);
intent.putExtra(PARAM_AUTOFINISH_TRANSACTIONS, false);
intent.putExtra(PARAM_PURCHASE_DATA, receipt);
intent.setAction(Action.FINISH_TRANSACTION.toString());
activity.startActivity(intent);
}
});
}
public void processPendingConsumables(final IPurchaseListener listener) {
this.activity.runOnUiThread(new Runnable() {
@Override
public void run() {
init();
IapGooglePlay.this.purchaseListener = listener;
Intent intent = new Intent(activity, IapGooglePlayActivity.class);
intent.putExtra(PARAM_MESSENGER, messenger);
intent.putExtra(PARAM_AUTOFINISH_TRANSACTIONS, IapGooglePlay.this.autoFinishTransactions);
intent.setAction(Action.PROCESS_PENDING_CONSUMABLES.toString());
activity.startActivity(intent);
}
});
}
public void restore(final IPurchaseListener listener) {
this.activity.runOnUiThread(new Runnable() {
@Override
public void run() {
init();
IapGooglePlay.this.purchaseListener = listener;
Intent intent = new Intent(activity, IapGooglePlayActivity.class);
intent.putExtra(PARAM_MESSENGER, messenger);
intent.putExtra(PARAM_AUTOFINISH_TRANSACTIONS, IapGooglePlay.this.autoFinishTransactions);
intent.setAction(Action.RESTORE.toString());
activity.startActivity(intent);
}
});
}
public static String toISO8601(final Date date) {
String formatted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(date);
return formatted.substring(0, 22) + ":" + formatted.substring(22);
}
private static String convertPurchase(String purchase, String signature) {
try {
JSONObject p = new JSONObject(purchase);
p.put("ident", p.get("productId"));
p.put("state", IapJNI.TRANS_STATE_PURCHASED);
// We check if orderId is actually set here, otherwise we return a blank string.
// This is what Google used to do, but after some updates around June/May 2016
// they stopped to include the orderId key at all for test purchases. See: DEF-1940
if (p.has("orderId")) {
p.put("trans_ident", p.get("orderId"));
} else {
p.put("trans_ident", "");
}
p.put("date", toISO8601(new Date(p.getLong("purchaseTime"))));
// Receipt is the complete json data
// http://robertomurray.co.uk/blog/2013/server-side-google-play-in-app-billing-receipt-validation-and-testing/
p.put("receipt", purchase);
p.put("signature", signature);
// TODO: How to simulate original_trans on iOS?
p.remove("packageName");
p.remove("orderId");
p.remove("productId");
p.remove("developerPayload");
p.remove("purchaseTime");
p.remove("purchaseState");
p.remove("purchaseToken");
return p.toString();
} catch (JSONException e) {
Log.wtf(TAG, "Failed to convert purchase json", e);
}
return null;
}
@Override
public boolean handleMessage(Message msg) {
Bundle bundle = msg.getData();
String actionString = bundle.getString("action");
if (actionString == null) {
return false;
}
if (purchaseListener == null) {
Log.wtf(TAG, "No purchase listener set");
return false;
}
Action action = Action.valueOf(actionString);
if (action == Action.BUY) {
int responseCode = bundle.getInt(RESPONSE_CODE);
String purchaseData = bundle.getString(RESPONSE_INAPP_PURCHASE_DATA);
String dataSignature = bundle.getString(RESPONSE_INAPP_SIGNATURE);
if (purchaseData != null && dataSignature != null) {
purchaseData = convertPurchase(purchaseData, dataSignature);
} else {
purchaseData = "";
}
purchaseListener.onPurchaseResult(responseCode, purchaseData);
} else if (action == Action.RESTORE) {
Bundle items = bundle.getBundle("items");
if (!items.containsKey(RESPONSE_INAPP_ITEM_LIST)) {
purchaseListener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, "");
return true;
}
ArrayList<String> ownedSkus = items.getStringArrayList(RESPONSE_INAPP_ITEM_LIST);
ArrayList<String> purchaseDataList = items.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST);
ArrayList<String> signatureList = items.getStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST);
for (int i = 0; i < ownedSkus.size(); ++i) {
int c = IapJNI.BILLING_RESPONSE_RESULT_OK;
String pd = convertPurchase(purchaseDataList.get(i), signatureList.get(i));
if (pd == null) {
pd = "";
c = IapJNI.BILLING_RESPONSE_RESULT_ERROR;
}
purchaseListener.onPurchaseResult(c, pd);
}
}
return true;
}
}