Almost done

This commit is contained in:
Björn Ritzl 2020-07-23 12:18:40 +02:00
parent fcbdd9f4a6
commit 83876a508b
8 changed files with 513 additions and 1344 deletions

View File

@ -20,6 +20,4 @@
</application> </application>
<uses-permission android:name="com.android.vending.BILLING" />
</manifest> </manifest>

View File

@ -0,0 +1,3 @@
dependencies {
implementation 'com.android.billingclient:billing:3.0.0'
}

View File

@ -52,7 +52,12 @@ static IAP g_IAP;
static int IAP_ProcessPendingTransactions(lua_State* L) static int IAP_ProcessPendingTransactions(lua_State* L)
{ {
//todo handle pending transactions if there is such thing on Android IAP* iap = &g_IAP;
JNIEnv* env = Attach();
env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_ProcessPendingConsumables, g_IAP.m_IAPJNI);
Detach();
return 0; return 0;
} }
@ -468,4 +473,3 @@ static dmExtension::Result FinalizeIAP(dmExtension::Params* params)
DM_DECLARE_EXTENSION(IAPExt, "IAP", 0, 0, InitializeIAP, UpdateIAP, 0, FinalizeIAP) DM_DECLARE_EXTENSION(IAPExt, "IAP", 0, 0, InitializeIAP, UpdateIAP, 0, FinalizeIAP)
#endif //DM_PLATFORM_ANDROID #endif //DM_PLATFORM_ANDROID

View File

@ -1,501 +0,0 @@
/*
* This file is auto-generated. DO NOT MODIFY.
* Original file: /Users/chmu/android_workspace/TestBilling/src/com/android/vending/billing/IInAppBillingService.aidl
*/
package com.android.vending.billing;
/**
* InAppBillingService is the service that provides in-app billing version 3 and beyond.
* This service provides the following features:
* 1. Provides a new API to get details of in-app items published for the app including
* price, type, title and description.
* 2. The purchase flow is synchronous and purchase information is available immediately
* after it completes.
* 3. Purchase information of in-app purchases is maintained within the Google Play system
* till the purchase is consumed.
* 4. An API to consume a purchase of an inapp item. All purchases of one-time
* in-app items are consumable and thereafter can be purchased again.
* 5. An API to get current purchases of the user immediately. This will not contain any
* consumed purchases.
*
* All calls will give a response code with the following possible values
* RESULT_OK = 0 - success
* RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
* RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
* RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
* RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
* RESULT_ERROR = 6 - Fatal error during the API action
* RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
* RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
*/
public interface IInAppBillingService extends android.os.IInterface
{
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.android.vending.billing.IInAppBillingService
{
private static final java.lang.String DESCRIPTOR = "com.android.vending.billing.IInAppBillingService";
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an com.android.vending.billing.IInAppBillingService interface,
* generating a proxy if needed.
*/
public static com.android.vending.billing.IInAppBillingService asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.android.vending.billing.IInAppBillingService))) {
return ((com.android.vending.billing.IInAppBillingService)iin);
}
return new com.android.vending.billing.IInAppBillingService.Stub.Proxy(obj);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_isBillingSupported:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
java.lang.String _arg1;
_arg1 = data.readString();
java.lang.String _arg2;
_arg2 = data.readString();
int _result = this.isBillingSupported(_arg0, _arg1, _arg2);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
case TRANSACTION_getSkuDetails:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
java.lang.String _arg1;
_arg1 = data.readString();
java.lang.String _arg2;
_arg2 = data.readString();
android.os.Bundle _arg3;
if ((0!=data.readInt())) {
_arg3 = android.os.Bundle.CREATOR.createFromParcel(data);
}
else {
_arg3 = null;
}
android.os.Bundle _result = this.getSkuDetails(_arg0, _arg1, _arg2, _arg3);
reply.writeNoException();
if ((_result!=null)) {
reply.writeInt(1);
_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
}
else {
reply.writeInt(0);
}
return true;
}
case TRANSACTION_getBuyIntent:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
java.lang.String _arg1;
_arg1 = data.readString();
java.lang.String _arg2;
_arg2 = data.readString();
java.lang.String _arg3;
_arg3 = data.readString();
java.lang.String _arg4;
_arg4 = data.readString();
android.os.Bundle _result = this.getBuyIntent(_arg0, _arg1, _arg2, _arg3, _arg4);
reply.writeNoException();
if ((_result!=null)) {
reply.writeInt(1);
_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
}
else {
reply.writeInt(0);
}
return true;
}
case TRANSACTION_getPurchases:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
java.lang.String _arg1;
_arg1 = data.readString();
java.lang.String _arg2;
_arg2 = data.readString();
java.lang.String _arg3;
_arg3 = data.readString();
android.os.Bundle _result = this.getPurchases(_arg0, _arg1, _arg2, _arg3);
reply.writeNoException();
if ((_result!=null)) {
reply.writeInt(1);
_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
}
else {
reply.writeInt(0);
}
return true;
}
case TRANSACTION_consumePurchase:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
java.lang.String _arg1;
_arg1 = data.readString();
java.lang.String _arg2;
_arg2 = data.readString();
int _result = this.consumePurchase(_arg0, _arg1, _arg2);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements com.android.vending.billing.IInAppBillingService
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
/**
* Checks support for the requested billing API version, package and in-app type.
* Minimum API version supported by this interface is 3.
* @param apiVersion the billing version which the app is using
* @param packageName the package name of the calling app
* @param type type of the in-app item being purchased "inapp" for one-time purchases
* and "subs" for subscription.
* @return RESULT_OK(0) on success, corresponding result code on failures
*/
@Override public int isBillingSupported(int apiVersion, java.lang.String packageName, java.lang.String type) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(apiVersion);
_data.writeString(packageName);
_data.writeString(type);
mRemote.transact(Stub.TRANSACTION_isBillingSupported, _data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
/**
* Provides details of a list of SKUs
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
* with a list JSON strings containing the productId, price, title and description.
* This API can be called with a maximum of 20 SKUs.
* @param apiVersion billing API version that the Third-party is using
* @param packageName the package name of the calling app
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "DETAILS_LIST" with a StringArrayList containing purchase information
* in JSON format similar to:
* '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
* "title : "Example Title", "description" : "This is an example description" }'
*/
@Override public android.os.Bundle getSkuDetails(int apiVersion, java.lang.String packageName, java.lang.String type, android.os.Bundle skusBundle) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
android.os.Bundle _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(apiVersion);
_data.writeString(packageName);
_data.writeString(type);
if ((skusBundle!=null)) {
_data.writeInt(1);
skusBundle.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_getSkuDetails, _data, _reply, 0);
_reply.readException();
if ((0!=_reply.readInt())) {
_result = android.os.Bundle.CREATOR.createFromParcel(_reply);
}
else {
_result = null;
}
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
/**
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
* the type, a unique purchase token and an optional developer payload.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param sku the SKU of the in-app item as published in the developer console
* @param type the type of the in-app item ("inapp" for one-time purchases
* and "subs" for subscription).
* @param developerPayload optional argument to be sent back with the purchase information
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "BUY_INTENT" - PendingIntent to start the purchase flow
*
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
* If the purchase is successful, the result data will contain the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
* '{"orderId":"12999763169054705758.1371079406387615",
* "packageName":"com.example.app",
* "productId":"exampleSku",
* "purchaseTime":1345678900000,
* "purchaseToken" : "122333444455555",
* "developerPayload":"example developer payload" }'
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
* was signed with the private key of the developer
* TODO: change this to app-specific keys.
*/
@Override public android.os.Bundle getBuyIntent(int apiVersion, java.lang.String packageName, java.lang.String sku, java.lang.String type, java.lang.String developerPayload) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
android.os.Bundle _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(apiVersion);
_data.writeString(packageName);
_data.writeString(sku);
_data.writeString(type);
_data.writeString(developerPayload);
mRemote.transact(Stub.TRANSACTION_getBuyIntent, _data, _reply, 0);
_reply.readException();
if ((0!=_reply.readInt())) {
_result = android.os.Bundle.CREATOR.createFromParcel(_reply);
}
else {
_result = null;
}
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
/**
* 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 in V3 and managed items purchased using
* V1 and V2 that have not been consumed.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param type the type of the in-app items being requested
* ("inapp" for one-time purchases and "subs" for subscription).
* @param continuationToken to be set as null for the first call, if the number of owned
* skus are too many, a continuationToken is returned in the response bundle.
* This method can be called again with the continuation token to get the next set of
* owned skus.
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
* of the purchase information
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
* next set of in-app purchases. Only set if the
* user has more owned skus than the current list.
*/
@Override public android.os.Bundle getPurchases(int apiVersion, java.lang.String packageName, java.lang.String type, java.lang.String continuationToken) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
android.os.Bundle _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(apiVersion);
_data.writeString(packageName);
_data.writeString(type);
_data.writeString(continuationToken);
mRemote.transact(Stub.TRANSACTION_getPurchases, _data, _reply, 0);
_reply.readException();
if ((0!=_reply.readInt())) {
_result = android.os.Bundle.CREATOR.createFromParcel(_reply);
}
else {
_result = null;
}
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
/**
* Consume the last purchase of the given SKU. This will result in this item being removed
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param purchaseToken token in the purchase information JSON that identifies the purchase
* to be consumed
* @return 0 if consumption succeeded. Appropriate error values for failures.
*/
@Override public int consumePurchase(int apiVersion, java.lang.String packageName, java.lang.String purchaseToken) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(apiVersion);
_data.writeString(packageName);
_data.writeString(purchaseToken);
mRemote.transact(Stub.TRANSACTION_consumePurchase, _data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
}
static final int TRANSACTION_isBillingSupported = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_getSkuDetails = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
static final int TRANSACTION_getBuyIntent = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
static final int TRANSACTION_getPurchases = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
static final int TRANSACTION_consumePurchase = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4);
}
/**
* Checks support for the requested billing API version, package and in-app type.
* Minimum API version supported by this interface is 3.
* @param apiVersion the billing version which the app is using
* @param packageName the package name of the calling app
* @param type type of the in-app item being purchased "inapp" for one-time purchases
* and "subs" for subscription.
* @return RESULT_OK(0) on success, corresponding result code on failures
*/
public int isBillingSupported(int apiVersion, java.lang.String packageName, java.lang.String type) throws android.os.RemoteException;
/**
* Provides details of a list of SKUs
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
* with a list JSON strings containing the productId, price, title and description.
* This API can be called with a maximum of 20 SKUs.
* @param apiVersion billing API version that the Third-party is using
* @param packageName the package name of the calling app
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "DETAILS_LIST" with a StringArrayList containing purchase information
* in JSON format similar to:
* '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
* "title : "Example Title", "description" : "This is an example description" }'
*/
public android.os.Bundle getSkuDetails(int apiVersion, java.lang.String packageName, java.lang.String type, android.os.Bundle skusBundle) throws android.os.RemoteException;
/**
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
* the type, a unique purchase token and an optional developer payload.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param sku the SKU of the in-app item as published in the developer console
* @param type the type of the in-app item ("inapp" for one-time purchases
* and "subs" for subscription).
* @param developerPayload optional argument to be sent back with the purchase information
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "BUY_INTENT" - PendingIntent to start the purchase flow
*
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
* If the purchase is successful, the result data will contain the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
* '{"orderId":"12999763169054705758.1371079406387615",
* "packageName":"com.example.app",
* "productId":"exampleSku",
* "purchaseTime":1345678900000,
* "purchaseToken" : "122333444455555",
* "developerPayload":"example developer payload" }'
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
* was signed with the private key of the developer
* TODO: change this to app-specific keys.
*/
public android.os.Bundle getBuyIntent(int apiVersion, java.lang.String packageName, java.lang.String sku, java.lang.String type, java.lang.String developerPayload) throws android.os.RemoteException;
/**
* 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 in V3 and managed items purchased using
* V1 and V2 that have not been consumed.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param type the type of the in-app items being requested
* ("inapp" for one-time purchases and "subs" for subscription).
* @param continuationToken to be set as null for the first call, if the number of owned
* skus are too many, a continuationToken is returned in the response bundle.
* This method can be called again with the continuation token to get the next set of
* owned skus.
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
* of the purchase information
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
* next set of in-app purchases. Only set if the
* user has more owned skus than the current list.
*/
public android.os.Bundle getPurchases(int apiVersion, java.lang.String packageName, java.lang.String type, java.lang.String continuationToken) throws android.os.RemoteException;
/**
* Consume the last purchase of the given SKU. This will result in this item being removed
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param purchaseToken token in the purchase information JSON that identifies the purchase
* to be consumed
* @return 0 if consumption succeeded. Appropriate error values for failures.
*/
public int consumePurchase(int apiVersion, java.lang.String packageName, java.lang.String purchaseToken) throws android.os.RemoteException;
}

View File

@ -5,507 +5,366 @@ import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.json.JSONArray;
import android.app.Activity; 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.content.pm.PackageManager;
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 android.util.Log;
import com.android.vending.billing.IInAppBillingService; import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClient.BillingResponseCode;
import com.android.billingclient.api.BillingClient.SkuType;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.Purchase.PurchasesResult;
import com.android.billingclient.api.Purchase.PurchaseState;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.SkuDetailsResponseListener;
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 class IapGooglePlay implements PurchasesUpdatedListener {
public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST"; public static final String TAG = "IapGooglePlay";
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 { private BillingClient billingClient;
BUY,
RESTORE,
PROCESS_PENDING_CONSUMABLES,
FINISH_TRANSACTION
}
public static final String TAG = "iap"; private Map<String, SkuDetails> products;
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 IPurchaseListener purchaseListener;
private boolean initialized;
private boolean autoFinishTransactions; private boolean autoFinishTransactions;
private Activity activity;
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) { public IapGooglePlay(Activity activity, boolean autoFinishTransactions) {
this.activity = activity; this.activity = activity;
this.autoFinishTransactions = autoFinishTransactions; this.autoFinishTransactions = autoFinishTransactions;
} products = new HashMap<String, SkuDetails>();
private static boolean isPlayStoreInstalled(Context context){
try {
context.getPackageManager().getPackageInfo("com.android.vending", 0);
return true;
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
private void init() {
// NOTE: We must create Handler lazily as construction of
// handlers must be in the context of a "looper" on Android
if (!isPlayStoreInstalled(activity)) {
Log.e(TAG, "Unable to find Google Play Store (com.android.vending)");
return;
}
if (this.initialized)
return;
this.initialized = true;
this.handler = new Handler(this);
this.messenger = new Messenger(this.handler);
serviceConn = new ServiceConnection() {
billingClient = BillingClient.newBuilder(activity).setListener(this).enablePendingPurchases().build();
billingClient.startConnection(new BillingClientStateListener() {
@Override @Override
public void onServiceDisconnected(ComponentName name) { public void onBillingSetupFinished(BillingResult billingResult) {
Log.v(TAG, "IAP disconnected"); if (billingResult.getResponseCode() == BillingResponseCode.OK) {
service = null; Log.v(TAG, "Setup finished");
} // TODO BillingClient.queryPurchases()
@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, final long commandPtr) {
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(), commandPtr);
}
catch(JSONException e) {
Log.wtf(TAG, "Failed to convert products", e);
listener.onProductsResult(resultCode, null, commandPtr);
}
} }
else { else {
listener.onProductsResult(resultCode, null, commandPtr); Log.wtf(TAG, "Setup error: " + billingResult.getDebugMessage());
} }
} }
}));
}
// 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 @Override
public void run() { public void onBillingServiceDisconnected() {
init(); Log.v(TAG, "Service disconnected");
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 String toISO8601(final Date date) {
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); String formatted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(date);
return formatted.substring(0, 22) + ":" + formatted.substring(22); return formatted.substring(0, 22) + ":" + formatted.substring(22);
} }
private static String convertPurchase(String purchase, String signature) { private String convertPurchase(Purchase purchase) {
// original JSON:
// {
// "orderId":"GPA.3301-1670-7033-37542",
// "packageName":"com.defold.extension.iap",
// "productId":"com.defold.iap.goldbar.medium",
// "purchaseTime":1595455967875,
// "purchaseState":0,
// "purchaseToken":"kacckamkehbbammphdcnhbme.AO-J1OxznnK6E8ILqaAgrPa-3sfaHny424R1e_ZJ2LkaJVsy-5aEOmHizw0vUp-017m8OUvw1rSvfAHbOog1fIvDGJmjaze3MEVFOh1ayJsNFfPDUGwMA_u_9rlV7OqX_nnIyDShH2KE5WrnMC0yQyw7sg5hfgeW6A",
// "acknowledged":false
// }
Log.d(TAG, "convertPurchase() original json: " + purchase.getOriginalJson());
JSONObject p = new JSONObject();
try { try {
JSONObject p = new JSONObject(purchase); JSONObject original = new JSONObject(purchase.getOriginalJson());
p.put("ident", p.get("productId")); p.put("ident", original.get("productId"));
p.put("state", IapJNI.TRANS_STATE_PURCHASED); p.put("state", purchaseStateToDefoldState(purchase.getPurchaseState()));
p.put("trans_ident", purchase.getOrderId());
// We check if orderId is actually set here, otherwise we return a blank string. p.put("date", toISO8601(new Date(purchase.getPurchaseTime())));
// This is what Google used to do, but after some updates around June/May 2016 p.put("receipt", purchase.getPurchaseToken());
// they stopped to include the orderId key at all for test purchases. See: DEF-1940 p.put("signature", purchase.getSignature());
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);
} }
catch (JSONException e) {
Log.wtf(TAG, "Failed to convert purchase", e);
}
return p.toString();
}
return null; private JSONObject convertSkuDetails(SkuDetails skuDetails) {
JSONObject p = new JSONObject();
try {
p.put("price_string", skuDetails.getPrice());
p.put("ident", skuDetails.getSku());
p.put("currency_code", skuDetails.getPriceCurrencyCode());
p.put("price", skuDetails.getPriceAmountMicros() * 0.000001);
}
catch(JSONException e) {
Log.wtf(TAG, "Failed to convert sku details", e);
}
return p;
}
private int purchaseStateToDefoldState(int purchaseState) {
int defoldState;
switch(purchaseState) {
case PurchaseState.PENDING:
defoldState = IapJNI.TRANS_STATE_PURCHASING;
break;
case PurchaseState.PURCHASED:
defoldState = IapJNI.TRANS_STATE_PURCHASED;
break;
default:
case PurchaseState.UNSPECIFIED_STATE:
defoldState = IapJNI.TRANS_STATE_UNVERIFIED;
break;
}
return defoldState;
}
private int billingResponseCodeToDefoldResponse(int responseCode)
{
int defoldResponse;
switch(responseCode) {
case BillingResponseCode.BILLING_UNAVAILABLE:
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE;
break;
case BillingResponseCode.DEVELOPER_ERROR:
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_DEVELOPER_ERROR;
break;
case BillingResponseCode.ITEM_ALREADY_OWNED:
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED;
break;
case BillingResponseCode.ITEM_NOT_OWNED:
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED;
break;
case BillingResponseCode.ITEM_UNAVAILABLE:
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE;
break;
case BillingResponseCode.OK:
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_OK;
break;
case BillingResponseCode.SERVICE_TIMEOUT:
case BillingResponseCode.SERVICE_UNAVAILABLE:
case BillingResponseCode.SERVICE_DISCONNECTED:
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE;
break;
case BillingResponseCode.USER_CANCELED:
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_USER_CANCELED;
break;
case BillingResponseCode.FEATURE_NOT_SUPPORTED:
case BillingResponseCode.ERROR:
default:
defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ERROR;
break;
}
return defoldResponse;
}
private int billingResultToDefoldResponse(BillingResult result) {
return billingResponseCodeToDefoldResponse(result.getResponseCode());
}
// -------------------------------------------
private List<Purchase> queryPurchases(final String type) {
PurchasesResult result = billingClient.queryPurchases(type);
if (result.getBillingResult().getResponseCode() != BillingResponseCode.OK) {
Log.e(TAG, "Unable to query pending purchases: " + result.getBillingResult().getDebugMessage());
return new ArrayList<Purchase>();
}
return result.getPurchasesList();
}
public void processPendingConsumables(final IPurchaseListener purchaseListener)
{
Log.d(TAG, "processPendingConsumables()");
List<Purchase> purchasesList = new ArrayList<Purchase>();
purchasesList.addAll(queryPurchases(SkuType.INAPP));
purchasesList.addAll(queryPurchases(SkuType.SUBS));
for (Purchase purchase : purchasesList) {
handlePurchase(purchase, purchaseListener);
}
}
// -------------------------------------------
private void consumePurchase(final Purchase purchase, final IPurchaseListener purchaseListener)
{
Log.d(TAG, "consumePurchase()");
ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
Log.d(TAG, "consumePurchase() onConsumeResponse " + billingResult.getResponseCode() + " purchaseToken: " + purchaseToken);
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
Log.e(TAG, "Unable to consume purchase: " + billingResult.getDebugMessage());
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
return;
}
}
});
}
public void finishTransaction(final String purchaseToken, final IPurchaseListener purchaseListener) {
Log.d(TAG, "finishTransaction() " + purchaseToken);
List<Purchase> purchasesList = new ArrayList<Purchase>();
purchasesList.addAll(queryPurchases(SkuType.INAPP));
purchasesList.addAll(queryPurchases(SkuType.SUBS));
for(Purchase p : purchasesList) {
Log.d(TAG, "finishTransaction() purchase: " + p.getOriginalJson());
if (p.getPurchaseToken().equals(purchaseToken)) {
consumePurchase(p, purchaseListener);
return;
}
}
Log.e(TAG, "Unable to find purchase for token: " + purchaseToken);
purchaseListener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, "");
}
// -------------------------------------------
private void handlePurchase(final Purchase purchase, final IPurchaseListener purchaseListener) {
if (this.autoFinishTransactions) {
consumePurchase(purchase, purchaseListener);
}
else {
purchaseListener.onPurchaseResult(billingResponseCodeToDefoldResponse(BillingResponseCode.OK), convertPurchase(purchase));
}
} }
@Override @Override
public boolean handleMessage(Message msg) { public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
Bundle bundle = msg.getData(); if (billingResult.getResponseCode() == BillingResponseCode.OK && purchases != null) {
for (Purchase purchase : purchases) {
String actionString = bundle.getString("action"); handlePurchase(purchase, this.purchaseListener);
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 = "";
} }
} else {
this.purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
}
}
purchaseListener.onPurchaseResult(responseCode, purchaseData); private void buyProduct(SkuDetails sku, final IPurchaseListener purchaseListener) {
} else if (action == Action.RESTORE) { this.purchaseListener = purchaseListener;
Bundle items = bundle.getBundle("items");
if (!items.containsKey(RESPONSE_INAPP_ITEM_LIST)) { BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
purchaseListener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, ""); .setSkuDetails(sku)
return true; .build();
}
ArrayList<String> ownedSkus = items.getStringArrayList(RESPONSE_INAPP_ITEM_LIST); BillingResult billingResult = billingClient.launchBillingFlow(this.activity, billingFlowParams);
ArrayList<String> purchaseDataList = items.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST); if (billingResult.getResponseCode() != BillingResponseCode.OK) {
ArrayList<String> signatureList = items.getStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST); Log.e(TAG, "Purchase failed: " + billingResult.getDebugMessage());
for (int i = 0; i < ownedSkus.size(); ++i) { purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
int c = IapJNI.BILLING_RESPONSE_RESULT_OK; }
String pd = convertPurchase(purchaseDataList.get(i), signatureList.get(i)); }
if (pd == null) {
pd = ""; public void buy(final String product, final IPurchaseListener purchaseListener) {
c = IapJNI.BILLING_RESPONSE_RESULT_ERROR; Log.d(TAG, "buy()");
SkuDetails sku = this.products.get(product);
if (sku != null) {
buyProduct(sku, purchaseListener);
}
else {
List<String> skuList = new ArrayList<String>();
skuList.add(product);
querySkuDetailsAsync(skuList, new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
buyProduct(skuDetailsList.get(0), purchaseListener);
}
else {
Log.e(TAG, "Unable to get product details before buying: " + billingResult.getDebugMessage());
purchaseListener.onPurchaseResult(billingResultToDefoldResponse(billingResult), "");
}
} }
purchaseListener.onPurchaseResult(c, pd); });
}
}
// -------------------------------------------
private void querySkuDetailsAsync(final List<String> skuList, final SkuDetailsResponseListener listener)
{
SkuDetailsResponseListener detailsListener = new SkuDetailsResponseListener() {
private List<SkuDetails> allSkuDetails = new ArrayList<SkuDetails>();
private int queries = 2;
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetails) {
// cache skus (cache will be used to speed up buying)
for (SkuDetails sd : skuDetails)
{
IapGooglePlay.this.products.put(sd.getSku(), sd);
}
//
allSkuDetails.addAll(skuDetails);
queries--;
if (queries == 0)
{
listener.onSkuDetailsResponse(billingResult, allSkuDetails);
}
}
};
billingClient.querySkuDetailsAsync(SkuDetailsParams.newBuilder().setSkusList(skuList).setType(SkuType.INAPP).build(), detailsListener);
billingClient.querySkuDetailsAsync(SkuDetailsParams.newBuilder().setSkusList(skuList).setType(SkuType.SUBS).build(), detailsListener);
}
public void listItems(final String products, final IListProductsListener productsListener, final long commandPtr)
{
Log.d(TAG, "listItems()");
// create list of skus from comma separated string
List<String> skuList = new ArrayList<String>();
for (String p : products.split(",")) {
if (p.trim().length() > 0) {
skuList.add(p);
} }
} }
return true;
querySkuDetailsAsync(skuList, new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetails) {
JSONArray a = new JSONArray();
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
for (SkuDetails sd : skuDetails)
{
a.put(convertSkuDetails(sd));
}
}
else {
Log.e(TAG, "Unable to list products: " + billingResult.getDebugMessage());
}
productsListener.onProductsResult(billingResultToDefoldResponse(billingResult), a.toString(), commandPtr);
}
});
}
// -------------------------------------------
public void stop()
{
Log.d(TAG, "stop()");
}
public void restore(final IPurchaseListener listener)
{
Log.d(TAG, "restore()");
} }
} }

View File

@ -1,371 +0,0 @@
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();
}
}

View File

@ -169,7 +169,7 @@ nodes {
} }
nodes { nodes {
position { position {
x: 485.0 x: 530.0
y: 56.0 y: 56.0
z: 0.0 z: 0.0
w: 1.0 w: 1.0
@ -199,7 +199,7 @@ nodes {
w: 1.0 w: 1.0
} }
type: TYPE_TEMPLATE type: TYPE_TEMPLATE
id: "reset" id: "restore"
layer: "" layer: ""
inherit_alpha: true inherit_alpha: true
alpha: 1.0 alpha: 1.0
@ -226,7 +226,7 @@ nodes {
w: 1.0 w: 1.0
} }
size { size {
x: 300.0 x: 200.0
y: 88.0 y: 88.0
z: 0.0 z: 0.0
w: 1.0 w: 1.0
@ -240,12 +240,12 @@ nodes {
type: TYPE_BOX type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA blend_mode: BLEND_MODE_ALPHA
texture: "button/button_normal" texture: "button/button_normal"
id: "reset/larrybutton" id: "restore/larrybutton"
xanchor: XANCHOR_NONE xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT adjust_mode: ADJUST_MODE_FIT
parent: "reset" parent: "restore"
layer: "" layer: ""
inherit_alpha: true inherit_alpha: true
slice9 { slice9 {
@ -258,6 +258,7 @@ nodes {
clipping_visible: true clipping_visible: true
clipping_inverted: false clipping_inverted: false
alpha: 1.0 alpha: 1.0
overridden_fields: 4
template_node_child: true template_node_child: true
size_mode: SIZE_MODE_MANUAL size_mode: SIZE_MODE_MANUAL
} }
@ -294,9 +295,9 @@ nodes {
} }
type: TYPE_TEXT type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA blend_mode: BLEND_MODE_ALPHA
text: "Reset" text: "Restore"
font: "larryfont" font: "larryfont"
id: "reset/larrylabel" id: "restore/larrylabel"
xanchor: XANCHOR_NONE xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER pivot: PIVOT_CENTER
@ -314,7 +315,7 @@ nodes {
} }
adjust_mode: ADJUST_MODE_FIT adjust_mode: ADJUST_MODE_FIT
line_break: false line_break: false
parent: "reset/larrybutton" parent: "restore/larrybutton"
layer: "" layer: ""
inherit_alpha: true inherit_alpha: true
alpha: 1.0 alpha: 1.0
@ -705,7 +706,7 @@ nodes {
} }
nodes { nodes {
position { position {
x: 159.0 x: 109.0
y: 56.0 y: 56.0
z: 0.0 z: 0.0
w: 1.0 w: 1.0
@ -762,7 +763,7 @@ nodes {
w: 1.0 w: 1.0
} }
size { size {
x: 300.0 x: 200.0
y: 88.0 y: 88.0
z: 0.0 z: 0.0
w: 1.0 w: 1.0
@ -794,6 +795,7 @@ nodes {
clipping_visible: true clipping_visible: true
clipping_inverted: false clipping_inverted: false
alpha: 1.0 alpha: 1.0
overridden_fields: 4
template_node_child: true template_node_child: true
size_mode: SIZE_MODE_MANUAL size_mode: SIZE_MODE_MANUAL
} }
@ -1019,6 +1021,165 @@ nodes {
text_leading: 1.0 text_leading: 1.0
text_tracking: 0.0 text_tracking: 0.0
} }
nodes {
position {
x: 320.0
y: 56.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_TEMPLATE
id: "pending"
layer: ""
inherit_alpha: true
alpha: 1.0
template: "/dirtylarry/button.gui"
template_node_child: false
}
nodes {
position {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 200.0
y: 88.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: "button/button_normal"
id: "pending/larrybutton"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
parent: "pending"
layer: ""
inherit_alpha: true
slice9 {
x: 32.0
y: 32.0
z: 32.0
w: 32.0
}
clipping_mode: CLIPPING_MODE_NONE
clipping_visible: true
clipping_inverted: false
alpha: 1.0
overridden_fields: 4
template_node_child: true
size_mode: SIZE_MODE_MANUAL
}
nodes {
position {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "Pending"
font: "larryfont"
id: "pending/larrylabel"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
outline {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
adjust_mode: ADJUST_MODE_FIT
line_break: false
parent: "pending/larrybutton"
layer: ""
inherit_alpha: true
alpha: 1.0
outline_alpha: 1.0
shadow_alpha: 1.0
overridden_fields: 8
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
}
material: "/builtins/materials/gui.material" material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT adjust_reference: ADJUST_REFERENCE_PARENT
max_nodes: 512 max_nodes: 512

View File

@ -36,11 +36,21 @@ local function log(fmt, ...)
gui.set_text(gui.get_node("log"), s) gui.set_text(gui.get_node("log"), s)
end end
local function process_pending_transactions()
log("iap.process_pending_transactions()")
iap.process_pending_transactions()
end
local function buy(id) local function buy(id)
log("iap.buy() " .. id) log("iap.buy() " .. id)
iap.buy(id) iap.buy(id)
end end
local function restore()
log("iap.restore()")
iap.restore()
end
local function list() local function list()
log("iap.list()") log("iap.list()")
for item, button in pairs(item_buttons) do for item, button in pairs(item_buttons) do
@ -67,7 +77,7 @@ end
local function buy_listener(self, transaction, error) local function buy_listener(self, transaction, error)
pprint(transaction, error) pprint(transaction)
if error then if error then
log("iap.buy() error %s - %s", tostring(error.error), tostring(error.reason)) log("iap.buy() error %s - %s", tostring(error.error), tostring(error.reason))
return return
@ -109,5 +119,11 @@ function on_input(self, action_id, action)
dirtylarry:button("list", action_id, action, function() dirtylarry:button("list", action_id, action, function()
list() list()
end) end)
dirtylarry:button("restore", action_id, action, function()
restore()
end)
dirtylarry:button("pending", action_id, action, function()
process_pending_transactions()
end)
end end
end end