From 79cb91dc842eded0fcbb562f127996759abeddc7 Mon Sep 17 00:00:00 2001
From: volzhs <volzhs@gmail.com>
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/android/java/src')

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<String> nKeys = Arrays.asList(list);
+		List<String> cKeys = Arrays.asList(mSkuDetails.get_keys());
+		ArrayList<String> fKeys = new ArrayList<String>();
+		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<String> skuList = new ArrayList<String>(Arrays.asList(list));
+				if (skuList.size() == 0) {
+					return;
+				}
+				// Split the sku list in blocks of no more than 20 elements.
+				ArrayList<ArrayList<String>> packs = new ArrayList<ArrayList<String>>();
+				ArrayList<String> tempList;
+				int n = skuList.size() / 20;
+				int mod = skuList.size() % 20;
+				for (int i = 0; i < n; i++) {
+					tempList = new ArrayList<String>();
+					for (String s : skuList.subList(i * 20, i * 20 + 20)) {
+						tempList.add(s);
+					}
+					packs.add(tempList);
+				}
+				if (mod != 0) {
+					tempList = new ArrayList<String>();
+					for (String s : skuList.subList(n * 20, n * 20 + mod)) {
+						tempList.add(s);
+					}
+					packs.add(tempList);
+
+					for (ArrayList<String> 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<String> 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