mirror of
https://github.com/defold/extension-iap
synced 2025-09-28 09:32:19 +02:00
Initial commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package com.defold.iap;
|
||||
|
||||
public interface IListProductsListener {
|
||||
public void onProductsResult(int resultCode, String productList);
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
package com.defold.iap;
|
||||
|
||||
public interface IPurchaseListener {
|
||||
public void onPurchaseResult(int responseCode, String purchaseData);
|
||||
}
|
307
extension-iap/src/java/com/defold/iap/IapAmazon.java
Normal file
307
extension-iap/src/java/com/defold/iap/IapAmazon.java
Normal file
@@ -0,0 +1,307 @@
|
||||
package com.defold.iap;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import com.amazon.device.iap.PurchasingService;
|
||||
import com.amazon.device.iap.PurchasingListener;
|
||||
import com.amazon.device.iap.model.ProductDataResponse;
|
||||
import com.amazon.device.iap.model.PurchaseUpdatesResponse;
|
||||
import com.amazon.device.iap.model.PurchaseResponse;
|
||||
import com.amazon.device.iap.model.UserDataResponse;
|
||||
import com.amazon.device.iap.model.RequestId;
|
||||
import com.amazon.device.iap.model.Product;
|
||||
import com.amazon.device.iap.model.Receipt;
|
||||
import com.amazon.device.iap.model.UserData;
|
||||
import com.amazon.device.iap.model.FulfillmentResult;
|
||||
|
||||
public class IapAmazon implements PurchasingListener {
|
||||
|
||||
public static final String TAG = "iap";
|
||||
|
||||
private HashMap<RequestId, IListProductsListener> listProductsListeners;
|
||||
private HashMap<RequestId, IPurchaseListener> purchaseListeners;
|
||||
|
||||
private Activity activity;
|
||||
private boolean autoFinishTransactions;
|
||||
|
||||
public IapAmazon(Activity activity, boolean autoFinishTransactions) {
|
||||
this.activity = activity;
|
||||
this.autoFinishTransactions = autoFinishTransactions;
|
||||
this.listProductsListeners = new HashMap<RequestId, IListProductsListener>();
|
||||
this.purchaseListeners = new HashMap<RequestId, IPurchaseListener>();
|
||||
PurchasingService.registerListener(activity, this);
|
||||
}
|
||||
|
||||
private void init() {
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
}
|
||||
|
||||
public void listItems(final String skus, final IListProductsListener listener) {
|
||||
final Set<String> skuSet = new HashSet<String>();
|
||||
for (String x : skus.split(",")) {
|
||||
if (x.trim().length() > 0) {
|
||||
if (!skuSet.contains(x)) {
|
||||
skuSet.add(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It might seem unconventional to hold the lock while doing the function call,
|
||||
// but it prevents a race condition, as the API does not allow supplying own
|
||||
// requestId which could be generated ahead of time.
|
||||
synchronized (listProductsListeners) {
|
||||
RequestId req = PurchasingService.getProductData(skuSet);
|
||||
if (req != null) {
|
||||
listProductsListeners.put(req, listener);
|
||||
} else {
|
||||
Log.e(TAG, "Did not expect a null requestId");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void buy(final String product, final IPurchaseListener listener) {
|
||||
synchronized (purchaseListeners) {
|
||||
RequestId req = PurchasingService.purchase(product);
|
||||
if (req != null) {
|
||||
purchaseListeners.put(req, listener);
|
||||
} else {
|
||||
Log.e(TAG, "Did not expect a null requestId");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void finishTransaction(final String receipt, final IPurchaseListener listener) {
|
||||
if(this.autoFinishTransactions) {
|
||||
return;
|
||||
}
|
||||
PurchasingService.notifyFulfillment(receipt, FulfillmentResult.FULFILLED);
|
||||
}
|
||||
|
||||
private void doGetPurchaseUpdates(final IPurchaseListener listener, final boolean reset) {
|
||||
synchronized (purchaseListeners) {
|
||||
RequestId req = PurchasingService.getPurchaseUpdates(reset);
|
||||
if (req != null) {
|
||||
purchaseListeners.put(req, listener);
|
||||
} else {
|
||||
Log.e(TAG, "Did not expect a null requestId");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void processPendingConsumables(final IPurchaseListener listener) {
|
||||
// reset = false means getting any new receipts since the last call.
|
||||
doGetPurchaseUpdates(listener, false);
|
||||
}
|
||||
|
||||
public void restore(final IPurchaseListener listener) {
|
||||
// reset = true means getting all transaction history, although consumables
|
||||
// are not included, only entitlements, after testing.
|
||||
doGetPurchaseUpdates(listener, true);
|
||||
}
|
||||
|
||||
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 JSONObject makeTransactionObject(final UserData user, final Receipt receipt, int state) throws JSONException {
|
||||
JSONObject transaction = new JSONObject();
|
||||
transaction.put("ident", receipt.getSku());
|
||||
transaction.put("state", state);
|
||||
transaction.put("date", toISO8601(receipt.getPurchaseDate()));
|
||||
transaction.put("trans_ident", receipt.getReceiptId());
|
||||
transaction.put("receipt", receipt.getReceiptId());
|
||||
|
||||
// Only for amazon (this far), but required for using their server side receipt validation.
|
||||
transaction.put("is_sandbox_mode", PurchasingService.IS_SANDBOX_MODE);
|
||||
transaction.put("user_id", user.getUserId());
|
||||
|
||||
// According to documentation, cancellation support has to be enabled per item, and this is
|
||||
// not officially supported by any other IAP provider, and it is not expected to be used here either.
|
||||
//
|
||||
// But enforcing the use of only non-cancelable items is not possible either; so include these flags
|
||||
// for completeness.
|
||||
if (receipt.getCancelDate() != null)
|
||||
transaction.put("cancel_date", toISO8601(receipt.getCancelDate()));
|
||||
transaction.put("canceled", receipt.isCanceled());
|
||||
return transaction;
|
||||
}
|
||||
|
||||
// This callback method is invoked when an ProductDataResponse is available for a request initiated by PurchasingService.getProductData(java.util.Set).
|
||||
@Override
|
||||
public void onProductDataResponse(ProductDataResponse productDataResponse) {
|
||||
RequestId reqId = productDataResponse.getRequestId();
|
||||
IListProductsListener listener;
|
||||
synchronized (this.listProductsListeners) {
|
||||
listener = this.listProductsListeners.get(reqId);
|
||||
if (listener == null) {
|
||||
Log.e(TAG, "No listener found for request " + reqId.toString());
|
||||
return;
|
||||
}
|
||||
this.listProductsListeners.remove(reqId);
|
||||
}
|
||||
|
||||
if (productDataResponse.getRequestStatus() != ProductDataResponse.RequestStatus.SUCCESSFUL) {
|
||||
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
|
||||
} else {
|
||||
Map<String, Product> products = productDataResponse.getProductData();
|
||||
try {
|
||||
JSONObject data = new JSONObject();
|
||||
for (Map.Entry<String, Product> entry : products.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Product product = entry.getValue();
|
||||
JSONObject item = new JSONObject();
|
||||
item.put("ident", product.getSku());
|
||||
item.put("title", product.getTitle());
|
||||
item.put("description", product.getDescription());
|
||||
if (product.getPrice() != null) {
|
||||
String priceString = product.getPrice();
|
||||
item.put("price_string", priceString);
|
||||
// Based on return values from getPrice: https://developer.amazon.com/public/binaries/content/assets/javadoc/in-app-purchasing-api/com/amazon/inapp/purchasing/item.html
|
||||
item.put("price", priceString.replaceAll("[^0-9.,]", ""));
|
||||
}
|
||||
data.put(key, item);
|
||||
}
|
||||
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_OK, data.toString());
|
||||
} catch (JSONException e) {
|
||||
listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience function for getting and removing a purchaseListener (used for more than one operation).
|
||||
private IPurchaseListener pickPurchaseListener(RequestId requestId) {
|
||||
synchronized (this.purchaseListeners) {
|
||||
IPurchaseListener listener = this.purchaseListeners.get(requestId);
|
||||
if (listener != null) {
|
||||
this.purchaseListeners.remove(requestId);
|
||||
return listener;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// This callback method is invoked when a PurchaseResponse is available for a purchase initiated by PurchasingService.purchase(String).
|
||||
@Override
|
||||
public void onPurchaseResponse(PurchaseResponse purchaseResponse) {
|
||||
|
||||
IPurchaseListener listener = pickPurchaseListener(purchaseResponse.getRequestId());
|
||||
if (listener == null) {
|
||||
Log.e(TAG, "No listener found for request: " + purchaseResponse.getRequestId().toString());
|
||||
return;
|
||||
}
|
||||
|
||||
int code;
|
||||
String data = null;
|
||||
String fulfilReceiptId = null;
|
||||
|
||||
switch (purchaseResponse.getRequestStatus()) {
|
||||
case SUCCESSFUL:
|
||||
{
|
||||
try {
|
||||
code = IapJNI.BILLING_RESPONSE_RESULT_OK;
|
||||
data = makeTransactionObject(purchaseResponse.getUserData(), purchaseResponse.getReceipt(), IapJNI.TRANS_STATE_PURCHASED).toString();
|
||||
fulfilReceiptId = purchaseResponse.getReceipt().getReceiptId();
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "JSON Exception occured: " + e.toString());
|
||||
code = IapJNI.BILLING_RESPONSE_RESULT_DEVELOPER_ERROR;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ALREADY_PURCHASED:
|
||||
code = IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED;
|
||||
break;
|
||||
case INVALID_SKU:
|
||||
code = IapJNI.BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE;
|
||||
break;
|
||||
case FAILED:
|
||||
case NOT_SUPPORTED:
|
||||
default:
|
||||
code = IapJNI.BILLING_RESPONSE_RESULT_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
listener.onPurchaseResult(code, data);
|
||||
|
||||
if (fulfilReceiptId != null && autoFinishTransactions) {
|
||||
PurchasingService.notifyFulfillment(fulfilReceiptId, FulfillmentResult.FULFILLED);
|
||||
}
|
||||
}
|
||||
|
||||
// This callback method is invoked when a PurchaseUpdatesResponse is available for a request initiated by PurchasingService.getPurchaseUpdates(boolean).
|
||||
@Override
|
||||
public void onPurchaseUpdatesResponse(PurchaseUpdatesResponse purchaseUpdatesResponse) {
|
||||
|
||||
// The documentation seems to be a little misguiding regarding how to handle this.
|
||||
// This call is in response to getPurchaseUpdates() which can be called in two modes
|
||||
//
|
||||
// 1) Get all receipts since last call (reset = true)
|
||||
// 2) Get the whole transaction history.
|
||||
//
|
||||
// The result can carry the flag hasMore() where it is required to call getPurchaseUpdates again. See docs:
|
||||
// https://developer.amazon.com/public/apis/earn/in-app-purchasing/docs-v2/implementing-iap-2.0
|
||||
//
|
||||
// Examples indicate it should be called with the same value for 'reset' the secon time around
|
||||
// but actual testing ends up in an infinite loop where the same results are returned over and over.
|
||||
//
|
||||
// So here getPurchaseUpdates is called with result=false to fetch the next round of receipts.
|
||||
|
||||
RequestId reqId = purchaseUpdatesResponse.getRequestId();
|
||||
IPurchaseListener listener = pickPurchaseListener(reqId);
|
||||
if (listener == null) {
|
||||
Log.e(TAG, "No listener found for request " + reqId.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
switch (purchaseUpdatesResponse.getRequestStatus()) {
|
||||
case SUCCESSFUL:
|
||||
{
|
||||
try {
|
||||
for (Receipt receipt : purchaseUpdatesResponse.getReceipts()) {
|
||||
JSONObject trans = makeTransactionObject(purchaseUpdatesResponse.getUserData(), receipt, IapJNI.TRANS_STATE_PURCHASED);
|
||||
listener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_OK, trans.toString());
|
||||
if(autoFinishTransactions) {
|
||||
PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.FULFILLED);
|
||||
}
|
||||
}
|
||||
if (purchaseUpdatesResponse.hasMore()) {
|
||||
doGetPurchaseUpdates(listener, false);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "JSON Exception occured: " + e.toString());
|
||||
listener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_DEVELOPER_ERROR, null);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FAILED:
|
||||
case NOT_SUPPORTED:
|
||||
default:
|
||||
listener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// This callback method is invoked when a UserDataResponse is available for a request initiated by PurchasingService.getUserData().
|
||||
@Override
|
||||
public void onUserDataResponse(UserDataResponse userDataResponse) {
|
||||
// Intentionally left un-implemented; not used.
|
||||
}
|
||||
}
|
496
extension-iap/src/java/com/defold/iap/IapGooglePlay.java
Normal file
496
extension-iap/src/java/com/defold/iap/IapGooglePlay.java
Normal file
@@ -0,0 +1,496 @@
|
||||
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;
|
||||
}
|
||||
}
|
371
extension-iap/src/java/com/defold/iap/IapGooglePlayActivity.java
Normal file
371
extension-iap/src/java/com/defold/iap/IapGooglePlayActivity.java
Normal file
@@ -0,0 +1,371 @@
|
||||
package com.defold.iap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentSender.SendIntentException;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
|
||||
import com.android.vending.billing.IInAppBillingService;
|
||||
import com.defold.iap.IapGooglePlay.Action;
|
||||
|
||||
public class IapGooglePlayActivity extends Activity {
|
||||
|
||||
private boolean hasPendingPurchases = false;
|
||||
private boolean autoFinishTransactions = true;
|
||||
private boolean isDone = false;
|
||||
private Messenger messenger;
|
||||
ServiceConnection serviceConn;
|
||||
IInAppBillingService service;
|
||||
|
||||
// NOTE: Code from "trivialdrivesample"
|
||||
int getResponseCodeFromBundle(Bundle b) {
|
||||
Object o = b.get(IapGooglePlay.RESPONSE_CODE);
|
||||
if (o == null) {
|
||||
Log.d(IapGooglePlay.TAG, "Bundle with null response code, assuming OK (known issue)");
|
||||
return IapJNI.BILLING_RESPONSE_RESULT_OK;
|
||||
} else if (o instanceof Integer)
|
||||
return ((Integer) o).intValue();
|
||||
else if (o instanceof Long)
|
||||
return (int) ((Long) o).longValue();
|
||||
else {
|
||||
Log.e(IapGooglePlay.TAG, "Unexpected type for bundle response code.");
|
||||
Log.e(IapGooglePlay.TAG, o.getClass().getName());
|
||||
throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendBuyError(int error) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("action", Action.BUY.toString());
|
||||
|
||||
bundle.putInt(IapGooglePlay.RESPONSE_CODE, error);
|
||||
Message msg = new Message();
|
||||
msg.setData(bundle);
|
||||
|
||||
try {
|
||||
messenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.wtf(IapGooglePlay.TAG, "Unable to send message", e);
|
||||
}
|
||||
this.finish();
|
||||
}
|
||||
|
||||
private void buy(String product, String productType) {
|
||||
// Flush any pending items, in order to be able to buy the same (new) product again
|
||||
processPendingConsumables();
|
||||
|
||||
try {
|
||||
Bundle buyIntentBundle = service.getBuyIntent(3, getPackageName(), product, productType, "");
|
||||
int response = getResponseCodeFromBundle(buyIntentBundle);
|
||||
if (response == IapJNI.BILLING_RESPONSE_RESULT_OK) {
|
||||
hasPendingPurchases = true;
|
||||
PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");
|
||||
startIntentSenderForResult(pendingIntent.getIntentSender(), 1001, new Intent(), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0));
|
||||
} else if (response == IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED) {
|
||||
sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED);
|
||||
} else {
|
||||
sendBuyError(response);
|
||||
}
|
||||
|
||||
} catch (RemoteException e) {
|
||||
Log.e(IapGooglePlay.TAG, String.format("Failed to buy", e));
|
||||
sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR);
|
||||
} catch (SendIntentException e) {
|
||||
Log.e(IapGooglePlay.TAG, String.format("Failed to buy", e));
|
||||
sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean consume(String purchaseData) {
|
||||
try {
|
||||
if (purchaseData == null) {
|
||||
Log.e(IapGooglePlay.TAG, String.format("Failed to consume purchase, purchaseData was null!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
JSONObject pd = new JSONObject(purchaseData);
|
||||
if (!pd.isNull("autoRenewing")) {
|
||||
Log.i(IapGooglePlay.TAG, "Will not consume purchase since it is a subscription.");
|
||||
return true;
|
||||
}
|
||||
String token = pd.getString("purchaseToken");
|
||||
int consumeResponse = service.consumePurchase(3, getPackageName(), token);
|
||||
if (consumeResponse == IapJNI.BILLING_RESPONSE_RESULT_OK) {
|
||||
return true;
|
||||
} else {
|
||||
Log.e(IapGooglePlay.TAG, String.format("Failed to consume purchase (%d)", consumeResponse));
|
||||
sendBuyError(consumeResponse);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Log.e(IapGooglePlay.TAG, "Failed to consume purchase", e);
|
||||
sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR);
|
||||
} catch (JSONException e) {
|
||||
Log.e(IapGooglePlay.TAG, "Failed to consume purchase", e);
|
||||
sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean processPurchase(String purchaseData, String signature)
|
||||
{
|
||||
if (this.autoFinishTransactions && !consume(purchaseData)) {
|
||||
Log.e(IapGooglePlay.TAG, "Failed to consume and send message");
|
||||
return false;
|
||||
}
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("action", Action.BUY.toString());
|
||||
bundle.putInt(IapGooglePlay.RESPONSE_CODE, IapJNI.BILLING_RESPONSE_RESULT_OK);
|
||||
bundle.putString(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA, purchaseData);
|
||||
bundle.putString(IapGooglePlay.RESPONSE_INAPP_SIGNATURE, signature);
|
||||
|
||||
Message msg = new Message();
|
||||
msg.setData(bundle);
|
||||
try {
|
||||
messenger.send(msg);
|
||||
return true;
|
||||
} catch (RemoteException e) {
|
||||
Log.wtf(IapGooglePlay.TAG, "Unable to send message", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Make buy response codes for all consumables not yet processed.
|
||||
private void processPendingConsumables() {
|
||||
try {
|
||||
// Note: subscriptions cannot be consumed
|
||||
// https://developer.android.com/google/play/billing/api.html#subs
|
||||
Bundle items = service.getPurchases(3, getPackageName(), "inapp", null);
|
||||
int response = getResponseCodeFromBundle(items);
|
||||
if (response == IapJNI.BILLING_RESPONSE_RESULT_OK) {
|
||||
ArrayList<String> purchaseDataList = items.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST);
|
||||
ArrayList<String> signatureList = items.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST);
|
||||
for (int i = 0; i < purchaseDataList.size(); ++i) {
|
||||
String purchaseData = purchaseDataList.get(i);
|
||||
String signature = signatureList.get(i);
|
||||
if (!processPurchase(purchaseData, signature)) {
|
||||
// abort and retry some other time
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Log.e(IapGooglePlay.TAG, "Failed to process purchase", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void restore() {
|
||||
int response = IapJNI.BILLING_RESPONSE_RESULT_ERROR;
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("action", Action.RESTORE.toString());
|
||||
|
||||
Bundle items = new Bundle();
|
||||
try {
|
||||
ArrayList<String> purchaseItemList = new ArrayList<String>();
|
||||
ArrayList<String> purchaseDataList = new ArrayList<String>();
|
||||
ArrayList<String> signatureList = new ArrayList<String>();
|
||||
|
||||
Bundle inapp = service.getPurchases(3, getPackageName(), "inapp", null);
|
||||
if (getResponseCodeFromBundle(inapp) == IapJNI.BILLING_RESPONSE_RESULT_OK) {
|
||||
purchaseItemList.addAll(inapp.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_ITEM_LIST));
|
||||
purchaseDataList.addAll(inapp.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST));
|
||||
signatureList.addAll(inapp.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST));
|
||||
}
|
||||
|
||||
Bundle subs = service.getPurchases(3, getPackageName(), "subs", null);
|
||||
if (getResponseCodeFromBundle(subs) == IapJNI.BILLING_RESPONSE_RESULT_OK) {
|
||||
purchaseItemList.addAll(subs.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_ITEM_LIST));
|
||||
purchaseDataList.addAll(subs.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST));
|
||||
signatureList.addAll(subs.getStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST));
|
||||
}
|
||||
|
||||
items.putStringArrayList(IapGooglePlay.RESPONSE_INAPP_ITEM_LIST, purchaseItemList);
|
||||
items.putStringArrayList(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA_LIST, purchaseDataList);
|
||||
items.putStringArrayList(IapGooglePlay.RESPONSE_INAPP_SIGNATURE_LIST, signatureList);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(IapGooglePlay.TAG, "Failed to restore purchases", e);
|
||||
}
|
||||
bundle.putBundle("items", items);
|
||||
|
||||
bundle.putInt(IapGooglePlay.RESPONSE_CODE, response);
|
||||
Message msg = new Message();
|
||||
msg.setData(bundle);
|
||||
|
||||
try {
|
||||
messenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.wtf(IapGooglePlay.TAG, "Unable to send message", e);
|
||||
}
|
||||
this.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
View view = new View(this);
|
||||
view.setBackgroundColor(0x10ffffff);
|
||||
setContentView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
|
||||
|
||||
Intent intent = getIntent();
|
||||
final Bundle extras = intent.getExtras();
|
||||
this.messenger = (Messenger) extras.getParcelable(IapGooglePlay.PARAM_MESSENGER);
|
||||
final Action action = Action.valueOf(intent.getAction());
|
||||
this.autoFinishTransactions = extras.getBoolean(IapGooglePlay.PARAM_AUTOFINISH_TRANSACTIONS);
|
||||
|
||||
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
|
||||
serviceIntent.setPackage("com.android.vending");
|
||||
List<ResolveInfo> intentServices = getPackageManager().queryIntentServices(serviceIntent, 0);
|
||||
if (intentServices != null && !intentServices.isEmpty()) {
|
||||
// service available to handle that Intent
|
||||
serviceConn = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
service = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder serviceBinder) {
|
||||
service = IInAppBillingService.Stub.asInterface(serviceBinder);
|
||||
if (action == Action.BUY) {
|
||||
buy(extras.getString(IapGooglePlay.PARAM_PRODUCT), extras.getString(IapGooglePlay.PARAM_PRODUCT_TYPE));
|
||||
} else if (action == Action.RESTORE) {
|
||||
restore();
|
||||
} else if (action == Action.PROCESS_PENDING_CONSUMABLES) {
|
||||
processPendingConsumables();
|
||||
finish();
|
||||
} else if (action == Action.FINISH_TRANSACTION) {
|
||||
consume(extras.getString(IapGooglePlay.PARAM_PURCHASE_DATA));
|
||||
finish();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bindService(serviceIntent, serviceConn, Context.BIND_AUTO_CREATE);
|
||||
} else {
|
||||
// Service will never be connected; just send unavailability message
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("action", intent.getAction());
|
||||
bundle.putInt(IapGooglePlay.RESPONSE_CODE, IapJNI.BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE);
|
||||
Message msg = new Message();
|
||||
msg.setData(bundle);
|
||||
try {
|
||||
messenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.wtf(IapGooglePlay.TAG, "Unable to send message", e);
|
||||
}
|
||||
this.finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
super.finish();
|
||||
this.isDone = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (hasPendingPurchases) {
|
||||
// Not sure connection is up so need to check here.
|
||||
if (service != null) {
|
||||
if(autoFinishTransactions) {
|
||||
processPendingConsumables();
|
||||
}
|
||||
}
|
||||
hasPendingPurchases = false;
|
||||
}
|
||||
|
||||
if( !isDone )
|
||||
{
|
||||
Intent intent = getIntent();
|
||||
|
||||
if( intent != null && intent.getComponent().getClassName().equals( getClass().getName() ) )
|
||||
{
|
||||
Log.v(IapGooglePlay.TAG, "There's still an intent left: " + intent.getAction() );
|
||||
sendBuyError(IapJNI.BILLING_RESPONSE_RESULT_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceConn != null) {
|
||||
try
|
||||
{
|
||||
unbindService(serviceConn);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.wtf(IapGooglePlay.TAG, "Unable to unbind service", e);
|
||||
}
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
// NOTE: Code from "trivialdrivesample"
|
||||
int getResponseCodeFromIntent(Intent i) {
|
||||
Object o = i.getExtras().get(IapGooglePlay.RESPONSE_CODE);
|
||||
if (o == null) {
|
||||
Log.e(IapGooglePlay.TAG, "Intent with no response code, assuming OK (known issue)");
|
||||
return IapJNI.BILLING_RESPONSE_RESULT_OK;
|
||||
} else if (o instanceof Integer) {
|
||||
return ((Integer) o).intValue();
|
||||
} else if (o instanceof Long) {
|
||||
return (int) ((Long) o).longValue();
|
||||
} else {
|
||||
Log.e(IapGooglePlay.TAG, "Unexpected type for intent response code.");
|
||||
Log.e(IapGooglePlay.TAG, o.getClass().getName());
|
||||
throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
Bundle bundle = null;
|
||||
if (data != null) {
|
||||
int responseCode = getResponseCodeFromIntent(data);
|
||||
String purchaseData = data.getStringExtra(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA);
|
||||
String dataSignature = data.getStringExtra(IapGooglePlay.RESPONSE_INAPP_SIGNATURE);
|
||||
if (responseCode == IapJNI.BILLING_RESPONSE_RESULT_OK) {
|
||||
processPurchase(purchaseData, dataSignature);
|
||||
} else {
|
||||
bundle = new Bundle();
|
||||
bundle.putString("action", Action.BUY.toString());
|
||||
bundle.putInt(IapGooglePlay.RESPONSE_CODE, responseCode);
|
||||
bundle.putString(IapGooglePlay.RESPONSE_INAPP_PURCHASE_DATA, purchaseData);
|
||||
bundle.putString(IapGooglePlay.RESPONSE_INAPP_SIGNATURE, dataSignature);
|
||||
}
|
||||
} else {
|
||||
bundle = new Bundle();
|
||||
bundle.putString("action", Action.BUY.toString());
|
||||
bundle.putInt(IapGooglePlay.RESPONSE_CODE, IapJNI.BILLING_RESPONSE_RESULT_ERROR);
|
||||
}
|
||||
|
||||
// Send message if generated above
|
||||
if (bundle != null) {
|
||||
Message msg = new Message();
|
||||
msg.setData(bundle);
|
||||
try {
|
||||
messenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.wtf(IapGooglePlay.TAG, "Unable to send message", e);
|
||||
}
|
||||
}
|
||||
|
||||
this.finish();
|
||||
}
|
||||
}
|
31
extension-iap/src/java/com/defold/iap/IapJNI.java
Normal file
31
extension-iap/src/java/com/defold/iap/IapJNI.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.defold.iap;
|
||||
|
||||
public class IapJNI implements IListProductsListener, IPurchaseListener {
|
||||
|
||||
// NOTE: Also defined in iap.h
|
||||
public static final int TRANS_STATE_PURCHASING = 0;
|
||||
public static final int TRANS_STATE_PURCHASED = 1;
|
||||
public static final int TRANS_STATE_FAILED = 2;
|
||||
public static final int TRANS_STATE_RESTORED = 3;
|
||||
public static final int TRANS_STATE_UNVERIFIED = 4;
|
||||
|
||||
public static final int BILLING_RESPONSE_RESULT_OK = 0;
|
||||
public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
|
||||
public static final int BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE = 2;
|
||||
public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
|
||||
public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
|
||||
public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
|
||||
public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
|
||||
public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
|
||||
public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
|
||||
|
||||
public IapJNI() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public native void onProductsResult(int responseCode, String productList);
|
||||
|
||||
@Override
|
||||
public native void onPurchaseResult(int responseCode, String purchaseData);
|
||||
|
||||
}
|
Reference in New Issue
Block a user