From f26f181ba964d4fa4903ab936c26a27c65a1e525 Mon Sep 17 00:00:00 2001 From: volzhs Date: Sun, 26 Jun 2016 02:37:50 +0900 Subject: Fix can't get all info if user purchases many items and not consumed --- .../godot/payments/PaymentsManager.java | 80 ++++++++++------------ 1 file changed, 36 insertions(+), 44 deletions(-) (limited to 'platform') diff --git a/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java b/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java index eb33b37ecc..35676e333e 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java +++ b/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java @@ -144,57 +144,49 @@ public class PaymentsManager { } }.consumeItAll(); } - - public void requestPurchased(){ - try{ + + public void requestPurchased() { + try { PaymentsCache pc = new PaymentsCache(Godot.getInstance()); -// Log.d("godot", "requestPurchased for " + activity.getPackageName()); - Bundle bundle = mService.getPurchases(3, activity.getPackageName(), "inapp",null); + String continueToken = null; -/* - for (String key : bundle.keySet()) { - Object value = bundle.get(key); - Log.d("godot", String.format("%s %s (%s)", key, value.toString(), value.getClass().getName())); - } -*/ - - if (bundle.getInt("RESPONSE_CODE") == 0){ + do { + Bundle bundle = mService.getPurchases(3, activity.getPackageName(), "inapp", continueToken); - final ArrayList myPurchases = bundle.getStringArrayList("INAPP_PURCHASE_DATA_LIST"); - final ArrayList mySignatures = bundle.getStringArrayList("INAPP_DATA_SIGNATURE_LIST"); - + if (bundle.getInt("RESPONSE_CODE") == 0) { - if (myPurchases == null || myPurchases.size() == 0){ -// Log.d("godot", "No purchases!"); - godotPaymentV3.callbackPurchased("", "", ""); - return; - } - -// Log.d("godot", "# products are purchased:" + myPurchases.size()); - for (int i=0;i myPurchases = bundle.getStringArrayList("INAPP_PURCHASE_DATA_LIST"); + final ArrayList mySignatures = bundle.getStringArrayList("INAPP_DATA_SIGNATURE_LIST"); + + if (myPurchases == null || myPurchases.size() == 0) { + godotPaymentV3.callbackPurchased("", "", ""); + return; } - } - } - }catch(Exception e){ + for (int i = 0; i < myPurchases.size(); i++) { + + try { + String receipt = myPurchases.get(i); + JSONObject inappPurchaseData = new JSONObject(receipt); + String sku = inappPurchaseData.getString("productId"); + String token = inappPurchaseData.getString("purchaseToken"); + String signature = mySignatures.get(i); + + pc.setConsumableValue("ticket_signautre", sku, signature); + pc.setConsumableValue("ticket", sku, receipt); + pc.setConsumableFlag("block", sku, true); + pc.setConsumableValue("token", sku, token); + + godotPaymentV3.callbackPurchased(receipt, signature, sku); + } catch (JSONException e) { + } + } + } + continueToken = bundle.getString("INAPP_CONTINUATION_TOKEN"); + Log.d("godot", "continue token = " + continueToken); + } while (!TextUtils.isEmpty(continueToken)); + } catch (Exception e) { Log.d("godot", "Error requesting purchased products:" + e.getClass().getName() + ":" + e.getMessage()); } } -- cgit v1.2.3 From 79cb91dc842eded0fcbb562f127996759abeddc7 Mon Sep 17 00:00:00 2001 From: volzhs Date: Sun, 26 Jun 2016 02:46:40 +0900 Subject: Add querying details of IAP items for android --- .../src/org/godotengine/godot/GodotPaymentV3.java | 172 ++++++++------ .../godot/payments/PaymentsManager.java | 255 ++++++++++++++------- 2 files changed, 279 insertions(+), 148 deletions(-) (limited to 'platform') diff --git a/platform/android/java/src/org/godotengine/godot/GodotPaymentV3.java b/platform/android/java/src/org/godotengine/godot/GodotPaymentV3.java index fde752acc9..b7de31ada3 100644 --- a/platform/android/java/src/org/godotengine/godot/GodotPaymentV3.java +++ b/platform/android/java/src/org/godotengine/godot/GodotPaymentV3.java @@ -28,99 +28,94 @@ /*************************************************************************/ package org.godotengine.godot; -import org.godotengine.godot.Dictionary; import android.app.Activity; import android.util.Log; +import org.godotengine.godot.payments.PaymentsManager; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + public class GodotPaymentV3 extends Godot.SingletonBase { private Godot activity; - private Integer purchaseCallbackId = 0; - private String accessToken; - private String purchaseValidationUrlPrefix; - private String transactionId; + private PaymentsManager mPaymentManager; + private Dictionary mSkuDetails = new Dictionary(); - public void purchase( String _sku, String _transactionId) { - final String sku = _sku; - final String transactionId = _transactionId; - activity.getPaymentsManager().setBaseSingleton(this); + public void purchase(final String sku, final String transactionId) { activity.runOnUiThread(new Runnable() { @Override public void run() { - activity.getPaymentsManager().requestPurchase(sku, transactionId); + mPaymentManager.requestPurchase(sku, transactionId); } }); } - -/* public string requestPurchasedTicket(){ - activity.getPaymentsManager() - } -*/ - static public Godot.SingletonBase initialize(Activity p_activity) { + static public Godot.SingletonBase initialize(Activity p_activity) { - return new GodotPaymentV3(p_activity); - } + return new GodotPaymentV3(p_activity); + } - public GodotPaymentV3(Activity p_activity) { - registerClass("GodotPayments", new String[] {"purchase", "setPurchaseCallbackId", "setPurchaseValidationUrlPrefix", "setTransactionId", "getSignature", "consumeUnconsumedPurchases", "requestPurchased", "setAutoConsume", "consume"}); - activity=(Godot) p_activity; + registerClass("GodotPayments", new String[]{"purchase", "setPurchaseCallbackId", "setPurchaseValidationUrlPrefix", "setTransactionId", "getSignature", "consumeUnconsumedPurchases", "requestPurchased", "setAutoConsume", "consume", "querySkuDetails"}); + activity = (Godot) p_activity; + mPaymentManager = activity.getPaymentsManager(); + mPaymentManager.setBaseSingleton(this); } - public void consumeUnconsumedPurchases(){ - activity.getPaymentsManager().setBaseSingleton(this); + public void consumeUnconsumedPurchases() { activity.runOnUiThread(new Runnable() { @Override public void run() { - activity.getPaymentsManager().consumeUnconsumedPurchases(); + mPaymentManager.consumeUnconsumedPurchases(); } }); } private String signature; - public String getSignature(){ - return this.signature; + + public String getSignature() { + return this.signature; } - - - public void callbackSuccess(String ticket, String signature, String sku){ -// Log.d(this.getClass().getName(), "PRE-Send callback to purchase success"); + + public void callbackSuccess(String ticket, String signature, String sku) { GodotLib.callobject(purchaseCallbackId, "purchase_success", new Object[]{ticket, signature, sku}); -// Log.d(this.getClass().getName(), "POST-Send callback to purchase success"); -} + } + + public void callbackSuccessProductMassConsumed(String ticket, String signature, String sku) { + Log.d(this.getClass().getName(), "callbackSuccessProductMassConsumed > " + ticket + "," + signature + "," + sku); + GodotLib.calldeferred(purchaseCallbackId, "consume_success", new Object[]{ticket, signature, sku}); + } - public void callbackSuccessProductMassConsumed(String ticket, String signature, String sku){ -// Log.d(this.getClass().getName(), "PRE-Send callback to consume success"); - Log.d(this.getClass().getName(), "callbackSuccessProductMassConsumed > "+ticket+","+signature+","+sku); - GodotLib.calldeferred(purchaseCallbackId, "consume_success", new Object[]{ticket, signature, sku}); -// Log.d(this.getClass().getName(), "POST-Send callback to consume success"); + public void callbackSuccessNoUnconsumedPurchases() { + GodotLib.calldeferred(purchaseCallbackId, "consume_not_required", new Object[]{}); } - public void callbackSuccessNoUnconsumedPurchases(){ - GodotLib.calldeferred(purchaseCallbackId, "no_validation_required", new Object[]{}); + public void callbackFailConsume() { + GodotLib.calldeferred(purchaseCallbackId, "consume_fail", new Object[]{}); } - - public void callbackFail(){ + + public void callbackFail() { GodotLib.calldeferred(purchaseCallbackId, "purchase_fail", new Object[]{}); -// GodotLib.callobject(purchaseCallbackId, "purchase_fail", new Object[]{}); } - - public void callbackCancel(){ + + public void callbackCancel() { GodotLib.calldeferred(purchaseCallbackId, "purchase_cancel", new Object[]{}); -// GodotLib.callobject(purchaseCallbackId, "purchase_cancel", new Object[]{}); } - - public void callbackAlreadyOwned(String sku){ + + public void callbackAlreadyOwned(String sku) { GodotLib.calldeferred(purchaseCallbackId, "purchase_owned", new Object[]{sku}); } - + public int getPurchaseCallbackId() { return purchaseCallbackId; } @@ -129,11 +124,11 @@ public class GodotPaymentV3 extends Godot.SingletonBase { this.purchaseCallbackId = purchaseCallbackId; } - public String getPurchaseValidationUrlPrefix(){ - return this.purchaseValidationUrlPrefix ; + public String getPurchaseValidationUrlPrefix() { + return this.purchaseValidationUrlPrefix; } - public void setPurchaseValidationUrlPrefix(String url){ + public void setPurchaseValidationUrlPrefix(String url) { this.purchaseValidationUrlPrefix = url; } @@ -144,39 +139,80 @@ public class GodotPaymentV3 extends Godot.SingletonBase { public void setAccessToken(String accessToken) { this.accessToken = accessToken; } - - public void setTransactionId(String transactionId){ + + public void setTransactionId(String transactionId) { this.transactionId = transactionId; } - - public String getTransactionId(){ + + public String getTransactionId() { return this.transactionId; } - + // request purchased items are not consumed - public void requestPurchased(){ - activity.getPaymentsManager().setBaseSingleton(this); + public void requestPurchased() { activity.runOnUiThread(new Runnable() { @Override public void run() { - activity.getPaymentsManager().requestPurchased(); + mPaymentManager.requestPurchased(); } }); } - + // callback for requestPurchased() - public void callbackPurchased(String receipt, String signature, String sku){ + public void callbackPurchased(String receipt, String signature, String sku) { GodotLib.calldeferred(purchaseCallbackId, "has_purchased", new Object[]{receipt, signature, sku}); } - + // consume item automatically after purchase. default is true. - public void setAutoConsume(boolean autoConsume){ - activity.getPaymentsManager().setAutoConsume(autoConsume); + public void setAutoConsume(boolean autoConsume) { + mPaymentManager.setAutoConsume(autoConsume); } - + // consume a specific item - public void consume(String sku){ - activity.getPaymentsManager().consume(sku); + public void consume(String sku) { + mPaymentManager.consume(sku); + } + + // query in app item detail info + public void querySkuDetails(String[] list) { + List nKeys = Arrays.asList(list); + List cKeys = Arrays.asList(mSkuDetails.get_keys()); + ArrayList fKeys = new ArrayList(); + for (String key : nKeys) { + if (!cKeys.contains(key)) { + fKeys.add(key); + } + } + if (fKeys.size() > 0) { + mPaymentManager.querySkuDetails(fKeys.toArray(new String[0])); + } else { + completeSkuDetail(); + } + } + + public void addSkuDetail(String itemJson) { + JSONObject o = null; + try { + o = new JSONObject(itemJson); + Dictionary item = new Dictionary(); + item.put("type", o.optString("type")); + item.put("product_id", o.optString("productId")); + item.put("title", o.optString("title")); + item.put("description", o.optString("description")); + item.put("price", o.optString("price")); + item.put("price_currency_code", o.optString("price_currency_code")); + item.put("price_amount", 0.000001d * o.optLong("price_amount_micros")); + mSkuDetails.put(item.get("product_id").toString(), item); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + public void completeSkuDetail() { + GodotLib.calldeferred(purchaseCallbackId, "sku_details_complete", new Object[]{mSkuDetails}); + } + + public void errorSkuDetail(String errorMessage) { + GodotLib.calldeferred(purchaseCallbackId, "sku_details_error", new Object[]{errorMessage}); } } - diff --git a/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java b/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java index 35676e333e..753c0a6f93 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java +++ b/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java @@ -28,119 +28,120 @@ /*************************************************************************/ package org.godotengine.godot.payments; -import java.util.ArrayList; -import java.util.List; - -import android.os.RemoteException; import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.os.Bundle; import android.os.IBinder; +import android.os.RemoteException; +import android.text.TextUtils; import android.util.Log; -import android.os.Bundle; -import org.json.JSONException; -import org.json.JSONObject; -import org.json.JSONStringer; +import com.android.vending.billing.IInAppBillingService; -import org.godotengine.godot.Dictionary; import org.godotengine.godot.Godot; import org.godotengine.godot.GodotPaymentV3; -import com.android.vending.billing.IInAppBillingService; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; public class PaymentsManager { public static final int BILLING_RESPONSE_RESULT_OK = 0; public static final int REQUEST_CODE_FOR_PURCHASE = 0x1001; private static boolean auto_consume = true; - + private Activity activity; IInAppBillingService mService; - public void setActivity(Activity activity){ + public void setActivity(Activity activity) { this.activity = activity; } - public static PaymentsManager createManager(Activity activity){ + public static PaymentsManager createManager(Activity activity) { PaymentsManager manager = new PaymentsManager(activity); return manager; } - - private PaymentsManager(Activity activity){ + + private PaymentsManager(Activity activity) { this.activity = activity; } - - public PaymentsManager initService(){ + + public PaymentsManager initService() { Intent intent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); intent.setPackage("com.android.vending"); activity.bindService( - intent, - mServiceConn, + intent, + mServiceConn, Context.BIND_AUTO_CREATE); return this; } - public void destroy(){ + public void destroy() { if (mService != null) { - activity.unbindService(mServiceConn); - } + activity.unbindService(mServiceConn); + } } - + ServiceConnection mServiceConn = new ServiceConnection() { - @Override - public void onServiceDisconnected(ComponentName name) { - mService = null; - } + @Override + public void onServiceDisconnected(ComponentName name) { + mService = null; + } - @Override - public void onServiceConnected(ComponentName name, IBinder service) { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { mService = IInAppBillingService.Stub.asInterface(service); - } + } }; - - public void requestPurchase(final String sku, String transactionId){ + + public void requestPurchase(final String sku, String transactionId) { new PurchaseTask(mService, Godot.getInstance()) { - + @Override protected void error(String message) { godotPaymentV3.callbackFail(); - + } - + @Override protected void canceled() { godotPaymentV3.callbackCancel(); } - + @Override protected void alreadyOwned() { godotPaymentV3.callbackAlreadyOwned(sku); } - + }.purchase(sku, transactionId); } - public void consumeUnconsumedPurchases(){ + public void consumeUnconsumedPurchases() { new ReleaseAllConsumablesTask(mService, activity) { - + @Override protected void success(String sku, String receipt, String signature, String token) { godotPaymentV3.callbackSuccessProductMassConsumed(receipt, signature, sku); } - + @Override protected void error(String message) { - godotPaymentV3.callbackFail(); - + Log.d("godot", "consumeUnconsumedPurchases :" + message); + godotPaymentV3.callbackFailConsume(); + } @Override protected void notRequired() { + Log.d("godot", "callbackSuccessNoUnconsumedPurchases :"); godotPaymentV3.callbackSuccessNoUnconsumedPurchases(); - + } }.consumeItAll(); } @@ -190,113 +191,207 @@ public class PaymentsManager { Log.d("godot", "Error requesting purchased products:" + e.getClass().getName() + ":" + e.getMessage()); } } - + public void processPurchaseResponse(int resultCode, Intent data) { - new HandlePurchaseTask(activity){ + new HandlePurchaseTask(activity) { @Override protected void success(final String sku, final String signature, final String ticket) { godotPaymentV3.callbackSuccess(ticket, signature, sku); - if (auto_consume){ + if (auto_consume) { new ConsumeTask(mService, activity) { - + @Override protected void success(String ticket) { -// godotPaymentV3.callbackSuccess(""); } - + @Override protected void error(String message) { godotPaymentV3.callbackFail(); - + } }.consume(sku); } - -// godotPaymentV3.callbackSuccess(new PaymentsCache(activity).getConsumableValue("ticket", sku),signature); -// godotPaymentV3.callbackSuccess(ticket); - //validatePurchase(purchaseToken, sku); } @Override protected void error(String message) { godotPaymentV3.callbackFail(); - } @Override protected void canceled() { godotPaymentV3.callbackCancel(); - } }.handlePurchaseRequest(resultCode, data); } - - public void validatePurchase(String purchaseToken, final String sku){ - - new ValidateTask(activity, godotPaymentV3){ + + public void validatePurchase(String purchaseToken, final String sku) { + + new ValidateTask(activity, godotPaymentV3) { @Override protected void success() { - + new ConsumeTask(mService, activity) { - + @Override protected void success(String ticket) { godotPaymentV3.callbackSuccess(ticket, null, sku); - } - + @Override protected void error(String message) { godotPaymentV3.callbackFail(); - } }.consume(sku); - + } @Override protected void error(String message) { godotPaymentV3.callbackFail(); - } @Override protected void canceled() { godotPaymentV3.callbackCancel(); - } }.validatePurchase(sku); } - - public void setAutoConsume(boolean autoConsume){ + + public void setAutoConsume(boolean autoConsume) { auto_consume = autoConsume; } - - public void consume(final String sku){ + + public void consume(final String sku) { new ConsumeTask(mService, activity) { - + @Override protected void success(String ticket) { godotPaymentV3.callbackSuccessProductMassConsumed(ticket, "", sku); - } - + @Override protected void error(String message) { - godotPaymentV3.callbackFail(); - + godotPaymentV3.callbackFailConsume(); } }.consume(sku); } - + + // Workaround to bug where sometimes response codes come as Long instead of Integer + int getResponseCodeFromBundle(Bundle b) { + Object o = b.get("RESPONSE_CODE"); + if (o == null) { + //logDebug("Bundle with null response code, assuming OK (known issue)"); + return BILLING_RESPONSE_RESULT_OK; + } else if (o instanceof Integer) return ((Integer) o).intValue(); + else if (o instanceof Long) return (int) ((Long) o).longValue(); + else { + //logError("Unexpected type for bundle response code."); + //logError(o.getClass().getName()); + throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName()); + } + } + + /** + * Returns a human-readable description for the given response code. + * + * @param code The response code + * @return A human-readable string explaining the result code. + * It also includes the result code numerically. + */ + public static String getResponseDesc(int code) { + String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" + + "3:Billing Unavailable/4:Item unavailable/" + + "5:Developer Error/6:Error/7:Item Already Owned/" + + "8:Item not owned").split("/"); + String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" + + "-1002:Bad response received/" + + "-1003:Purchase signature verification failed/" + + "-1004:Send intent failed/" + + "-1005:User cancelled/" + + "-1006:Unknown purchase response/" + + "-1007:Missing token/" + + "-1008:Unknown error/" + + "-1009:Subscriptions not available/" + + "-1010:Invalid consumption attempt").split("/"); + + if (code <= -1000) { + int index = -1000 - code; + if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index]; + else return String.valueOf(code) + ":Unknown IAB Helper Error"; + } else if (code < 0 || code >= iab_msgs.length) + return String.valueOf(code) + ":Unknown"; + else + return iab_msgs[code]; + } + + public void querySkuDetails(final String[] list) { + (new Thread(new Runnable() { + @Override + public void run() { + ArrayList skuList = new ArrayList(Arrays.asList(list)); + if (skuList.size() == 0) { + return; + } + // Split the sku list in blocks of no more than 20 elements. + ArrayList> packs = new ArrayList>(); + ArrayList tempList; + int n = skuList.size() / 20; + int mod = skuList.size() % 20; + for (int i = 0; i < n; i++) { + tempList = new ArrayList(); + for (String s : skuList.subList(i * 20, i * 20 + 20)) { + tempList.add(s); + } + packs.add(tempList); + } + if (mod != 0) { + tempList = new ArrayList(); + for (String s : skuList.subList(n * 20, n * 20 + mod)) { + tempList.add(s); + } + packs.add(tempList); + + for (ArrayList skuPartList : packs) { + Bundle querySkus = new Bundle(); + querySkus.putStringArrayList("ITEM_ID_LIST", skuPartList); + Bundle skuDetails = null; + try { + skuDetails = mService.getSkuDetails(3, activity.getPackageName(), "inapp", querySkus); + if (!skuDetails.containsKey("DETAILS_LIST")) { + int response = getResponseCodeFromBundle(skuDetails); + if (response != BILLING_RESPONSE_RESULT_OK) { + godotPaymentV3.errorSkuDetail(getResponseDesc(response)); + } else { + godotPaymentV3.errorSkuDetail("No error but no detail list."); + } + return; + } + + ArrayList responseList = skuDetails.getStringArrayList("DETAILS_LIST"); + + for (String thisResponse : responseList) { + Log.d("godot", "response = "+thisResponse); + godotPaymentV3.addSkuDetail(thisResponse); + } + } catch (RemoteException e) { + e.printStackTrace(); + godotPaymentV3.errorSkuDetail("RemoteException error!"); + } + } + godotPaymentV3.completeSkuDetail(); + } + } + })).start(); + } + private GodotPaymentV3 godotPaymentV3; - + public void setBaseSingleton(GodotPaymentV3 godotPaymentV3) { this.godotPaymentV3 = godotPaymentV3; } } - -- cgit v1.2.3