diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 585cc8cf2..193227601 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,7 +13,7 @@ jobs: - uses: krdlab/setup-haxe@v1 with: - haxe-version: 4.3.0 + haxe-version: 4.3.4 - name: Install haxelibs run: | diff --git a/src/nme/store/BillingManager.hx b/src/nme/store/BillingManager.hx index 7ced7b960..858b9793e 100644 --- a/src/nme/store/BillingManager.hx +++ b/src/nme/store/BillingManager.hx @@ -91,6 +91,7 @@ class BillingManager public static function setPurchases(inPurchases:Array, andFire=true) { purchases = inPurchases; + //trace("setPurchases " + inPurchases); if (andFire) fire( PurchasesUpdated(0) ); } diff --git a/src/nme/store/SkuInfo.hx b/src/nme/store/SkuInfo.hx index 0fc6adf19..dd6d589df 100644 --- a/src/nme/store/SkuInfo.hx +++ b/src/nme/store/SkuInfo.hx @@ -3,16 +3,17 @@ package nme.store; class SkuInfo { public var description:String; - public var freeTrialPeriod:String; - public var introductoryPrice:String; - public var introductoryPriceAmountMicros:String; - public var introductoryPriceCycles:String; - public var introductoryPricePeriod:String; + //public var freeTrialPeriod:String; + //public var introductoryPrice:String; + //public var introductoryPriceAmountMicros:String; + //public var introductoryPriceCycles:String; + //public var introductoryPricePeriod:String; + public var name:String; public var price:String; public var priceAmountMicros:String; public var priceCurrencyCode:String; public var sku:String; - public var subscriptionPeriod:String; + //public var subscriptionPeriod:String; public var title:String; public var type:String; diff --git a/templates/android/java/org/haxe/nme/BillingManager.java b/templates/android/java/org/haxe/nme/BillingManager.java index 597db20a0..c4acef477 100644 --- a/templates/android/java/org/haxe/nme/BillingManager.java +++ b/templates/android/java/org/haxe/nme/BillingManager.java @@ -24,6 +24,8 @@ import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.BillingClient.BillingResponseCode; import com.android.billingclient.api.BillingClient.FeatureType; +import com.android.billingclient.api.BillingClient.ProductType; +import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams; //import com.android.billingclient.api.BillingClient.SkuType; import com.android.billingclient.api.BillingClientStateListener; import com.android.billingclient.api.BillingFlowParams; @@ -31,9 +33,14 @@ import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.Purchase.PurchaseState; +import com.android.billingclient.api.ProductDetailsResponseListener; +import com.android.billingclient.api.PurchasesResponseListener; //import com.android.billingclient.api.Purchase.PurchasesResult; import com.android.billingclient.api.PurchasesUpdatedListener; -//import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.ProductDetails; +import com.android.billingclient.api.QueryPurchasesParams; +import com.android.billingclient.api.QueryProductDetailsParams; +import com.android.billingclient.api.QueryProductDetailsParams.Product; //import com.android.billingclient.api.SkuDetailsParams; //import com.android.billingclient.api.SkuDetailsResponseListener; import com.android.billingclient.api.AcknowledgePurchaseResponseListener; @@ -82,7 +89,7 @@ public class BillingManager implements PurchasesUpdatedListener { /** * True if billing service is connected now. */ - private boolean mIsServiceConnected; + private boolean mIsServiceConnected = false; private final HaxeObject mBillingUpdatesListener; @@ -120,70 +127,109 @@ public BillingManager(Activity activity, final String inPublicKey, final HaxeObj // once setup completes. // It also starts to report all the new purchases through onPurchasesUpdated() callback. startServiceConnection(new Runnable() { - @Override - public void run() { + @Override public void run() { + Log.d(TAG, "Billing client - started"); + GameActivity.sendHaxe( new Runnable() { + @Override + public void run() { + // Notifying the listener that billing client is ready + mBillingUpdatesListener.call0("onBillingClientSetupFinished"); + //if (purchaseCap!="") + // mBillingUpdatesListener.call2("onPurchasesUpdated", 0, purchaseCap); + } } ); + + } } ); + + } + + + public static JSONObject getProductJson(ProductDetails sku) throws JSONException + { + JSONObject obj= new JSONObject(); + obj.put("description", sku.getDescription() ); + obj.put("sku", sku.getProductId() ); + obj.put("title", sku.getTitle() ); + obj.put("type", sku.getProductType() ); + if (sku.getProductType().equals(ProductType.INAPP)) + { + ProductDetails.OneTimePurchaseOfferDetails d = sku.getOneTimePurchaseOfferDetails(); + obj.put("price", d.getFormattedPrice() ); + obj.put("priceAmountMicros", d.getPriceAmountMicros() ); + obj.put("priceCurrencyCode", d.getPriceCurrencyCode() ); + } + obj.put("name", sku.getName() ); - ::if ANDROID_INAPP_PRODUCT:: - ImmutableList productList = ImmutableList.of(Product.newBuilder() - .setProductId("up_basic_sub") - .setProductType(ProductType.SUBS) - .build()); - - QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder() - .setProductList(productList) - .build(); - - mBillingClient.queryProductDetailsAsync( - params, +/* + obj.put("freeTrialPeriod", sku.getFreeTrialPeriod() ); + obj.put("introductoryPrice", sku.getIntroductoryPrice() ); + obj.put("introductoryPriceAmountMicros", sku.getIntroductoryPriceAmountMicros() ); + obj.put("introductoryPriceCycles", sku.getIntroductoryPriceCycles() ); + obj.put("introductoryPricePeriod", sku.getIntroductoryPricePeriod() ); + obj.put("subscriptionPeriod", sku.getSubscriptionPeriod() ); +*/ + return obj; + } + + public void queryProductsAsync(final String type, String [] products, final HaxeObject onResult) + { + Log.d(TAG, "Billing client - queryProductsAsync " + products.length); + List plist = new ArrayList(); + + for(String p : products) + { + //Log.d(TAG, "Billing type " + type + "/" + ProductType.SUBS + "/" + ProductType.INAPP); + plist.add(Product.newBuilder().setProductId(p).setProductType(type).build()); + } + + final QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder() + .setProductList(plist).build(); + + final QueryPurchasesParams queryPurchasesParams = QueryPurchasesParams.newBuilder().setProductType(type).build(); + + final BillingManager man = this; + + startServiceConnection(new Runnable() { + @Override public void run() { + + mBillingClient.queryProductDetailsAsync( params, new ProductDetailsResponseListener() { public void onProductDetailsResponse(BillingResult billingResult, List productDetailsList) { - int billingResponseCode = billingResult.getResponseCode(); - if (billingResponseCode == BillingResponseCode.OK) { - // Process the result + + //Log.e(TAG, "onProductDetailsResponse"); + int responseCode = billingResult.getResponseCode(); + String result = ""; + try { + JSONArray array= new JSONArray(); + for(ProductDetails sku : productDetailsList) + { + JSONObject obj = getProductJson(sku); + array.put(obj); } + result = array.toString(); + //Log.e(TAG, " result=" + result ); + + } catch (JSONException e) { + Log.e(TAG, GameActivity.getStackTrace(e)); + responseCode = -1; } - }); - ::end:: + final int code = responseCode; + final String skus = result; + GameActivity.queueRunnable( new Runnable() { + @Override public void run() { + onResult.call2("onSkuDetails", code, skus); + } } ); + } } ); - /* - Purchase.PurchasesResult resSubs = mBillingClient.queryPurchases(SkuType.SUBS); - Purchase.PurchasesResult resInApp = mBillingClient.queryPurchases(SkuType.INAPP); - - String purchases = ""; - //if (resSubs.getResponseCode()==0 || resInApp.getResponseCode()==0) - { - try - { - JSONArray array = new JSONArray(); - for(Purchase purchase : resSubs.getPurchasesList()) - handlePurchase(purchase,array); - for(Purchase purchase : resInApp.getPurchasesList()) - handlePurchase(purchase,array); - purchases = array.toString(); - } - catch (JSONException e) - { - Log.e(TAG, "Error in handling purchase"); - Log.e(TAG, GameActivity.getStackTrace(e)); - } - } - final String purchaseCap = purchases; + mBillingClient.queryPurchasesAsync(queryPurchasesParams, + new PurchasesResponseListener() { + public void onQueryPurchasesResponse( BillingResult billingResult, List purchases) { + man.onPurchasesUpdated(billingResult, purchases); - GameActivity.sendHaxe( new Runnable() { - @Override - public void run() { - // Notifying the listener that billing client is ready - mBillingUpdatesListener.call0("onBillingClientSetupFinished"); - if (purchaseCap!="") - mBillingUpdatesListener.call2("onPurchasesUpdated", 0, purchaseCap); - } } ); - // IAB is fully set up. Now, let's get an inventory of stuff we own. - // - need an explicit billingQuery now - } - */ - }); + } } ); + + } } ); } @@ -194,6 +240,7 @@ public void run() { public void onPurchasesUpdated(BillingResult billingResult, List purchases) { int resultCode = billingResult.getResponseCode(); + //Log.w(TAG,"onPurchasesUpdated:" + resultCode); String result = ""; try @@ -234,38 +281,45 @@ void failedPurchase(final String sku, final int resultCode) */ public void initiatePurchaseFlow(final String productId, final String billingType) { - /* Runnable queryRequest = new Runnable() { @Override public void run() { // Query the purchase async - SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder(); - params.setSkusList( Arrays.asList(skuId) ).setType(billingType); - // Find SkuDetails for sku ... - mBillingClient.querySkuDetailsAsync(params.build(), - new SkuDetailsResponseListener() { - @Override - public void onSkuDetailsResponse(BillingResult result, - List skuDetailsList) { - - if (result.getResponseCode()!=BillingResponseCode.OK) + //SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder(); + //params.setSkusList( Arrays.asList(skuId) ).setType(billingType); + //mBillingClient.querySkuDetailsAsync(params.build(), + + Product p = Product.newBuilder().setProductId(productId).setProductType(billingType).build(); + final QueryProductDetailsParams prodParams = QueryProductDetailsParams.newBuilder() + .setProductList( Arrays.asList(p)).build(); + + mBillingClient.queryProductDetailsAsync(prodParams, + new ProductDetailsResponseListener() { + public void onProductDetailsResponse(BillingResult billingResult, + final List productDetailsList) { + + if (billingResult.getResponseCode()!=BillingResponseCode.OK) { - failedPurchase(skuId, result.getResponseCode()); + failedPurchase(productId, billingResult.getResponseCode()); } else { - if (skuDetailsList.size()!=1) + if (productDetailsList.size()!=1) { - failedPurchase(skuId, -100 - skuDetailsList.size()); + failedPurchase(productId, -100 - productDetailsList.size()); } else { // Now launch - final SkuDetails skuDetails = skuDetailsList.get(0); + + final ProductDetails productDetails = productDetailsList.get(0); + final ProductDetailsParams productDetailsParams + = ProductDetailsParams.newBuilder().setProductDetails(productDetails).build(); Runnable purchaseFlowRequest = new Runnable() { @Override public void run() { + BillingFlowParams purchaseParams = BillingFlowParams.newBuilder() - .setSkuDetails(skuDetails).build(); + .setProductDetailsParamsList( Arrays.asList(productDetailsParams) ).build(); mBillingClient.launchBillingFlow(mActivity, purchaseParams); } }; @@ -279,7 +333,6 @@ public void onSkuDetailsResponse(BillingResult result, }; executeServiceRequest(queryRequest); - */ } public Context getContext() { @@ -298,32 +351,6 @@ public void destroy() { } } - /* - public void querySkuDetailsAsync(@SkuType final String itemType, final List skuList, - final SkuDetailsResponseListener listener) - { - - // Creating a runnable from the request to use it inside our connection retry policy below - Runnable queryRequest = new Runnable() { - @Override - public void run() { - // Query the purchase async - SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder(); - params.setSkusList(skuList).setType(itemType); - mBillingClient.querySkuDetailsAsync(params.build(), - new SkuDetailsResponseListener() { - @Override - public void onSkuDetailsResponse(BillingResult responseCode, - List skuDetailsList) { - listener.onSkuDetailsResponse(responseCode, skuDetailsList); - } - }); - } - }; - - executeServiceRequest(queryRequest); - } - */ public void consumeAsync(final String purchaseToken) { // If we've already scheduled to consume this token - no action is needed (this could happen @@ -384,20 +411,24 @@ private void handlePurchase(Purchase purchase, JSONArray outList) throws JSONE { boolean valid = verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature()); - JSONObject obj= new JSONObject(); - //obj.put("sku", purchase.getSku() ); - obj.put("valid", valid ); - obj.put("purchaseToken", purchase.getPurchaseToken() ); - obj.put("orderId", purchase.getOrderId() ); - obj.put("packageName", purchase.getPackageName() ); - // 0=unknown, 1=purchased, 2=pending - obj.put("purchaseState", purchase.getPurchaseState() ); - obj.put("purchaseTime", purchase.getPurchaseTime() ); - obj.put("signature", purchase.getSignature() ); - obj.put("isAcknowledged", purchase.isAcknowledged() ); - obj.put("isAutoRenewing", purchase.isAutoRenewing() ); - - outList.put(obj); + Log.w(TAG, "handlePurchase " + purchase.getProducts() ); + for(String sku : purchase.getProducts() ) + { + JSONObject obj= new JSONObject(); + obj.put("sku", sku); + obj.put("valid", valid ); + obj.put("purchaseToken", purchase.getPurchaseToken() ); + obj.put("orderId", purchase.getOrderId() ); + obj.put("packageName", purchase.getPackageName() ); + // 0=unknown, 1=purchased, 2=pending + obj.put("purchaseState", purchase.getPurchaseState() ); + obj.put("purchaseTime", purchase.getPurchaseTime() ); + obj.put("signature", purchase.getSignature() ); + obj.put("isAcknowledged", purchase.isAcknowledged() ); + obj.put("isAutoRenewing", purchase.isAutoRenewing() ); + + outList.put(obj); + } } @@ -421,23 +452,29 @@ public void onAcknowledgePurchaseResponse(BillingResult billingResult) { } } public void startServiceConnection(final Runnable executeOnSuccess) { + //Log.e(TAG,"startConnection ..."); mBillingClient.startConnection(new BillingClientStateListener() { @Override public void onBillingSetupFinished(BillingResult billingResult) { int billingResponseCode = billingResult.getResponseCode(); + //Log.w(TAG, "onBillingSetupFinished " + billingResponseCode); if (billingResponseCode == BillingResponseCode.OK) { mIsServiceConnected = true; if (executeOnSuccess != null) { executeOnSuccess.run(); } } + else { + Log.w(TAG, "onBillingSetup could not setup billing" + billingResponseCode); + } mBillingClientResponseCode = billingResponseCode; } @Override public void onBillingServiceDisconnected() { + Log.w(TAG, "onBillingServiceDisconnected"); mIsServiceConnected = false; } }); diff --git a/templates/android/java/org/haxe/nme/GameActivity.java b/templates/android/java/org/haxe/nme/GameActivity.java index a9c2af05a..d32ff9afb 100644 --- a/templates/android/java/org/haxe/nme/GameActivity.java +++ b/templates/android/java/org/haxe/nme/GameActivity.java @@ -282,6 +282,7 @@ public static void billingInit(final String inPublicKey, final HaxeObject inUpda final GameActivity me = activity; queueRunnable( new Runnable() { @Override public void run() { me.mBillingManager = new BillingManager(me, inPublicKey, inUpdatesListener); + Log.v(TAG,"billingInit got:" + me.mBillingManager ); } }); } public static void billingClose() @@ -318,8 +319,9 @@ public static JSONObject getSkuJson(com.android.billingclient.api.SkuDetails sku } */ - public static void billingQuery(String itemType, String[] skuArray, final HaxeObject onResult) + public static void billingQuery(String productType, String[] products, final HaxeObject onResult) { + activity.mBillingManager.queryProductsAsync(productType, products, onResult); /* activity.mBillingManager.querySkuDetailsAsync(itemType, java.util.Arrays.asList(skuArray), new com.android.billingclient.api.SkuDetailsResponseListener() { diff --git a/tools/nme/src/CommandLineTools.hx b/tools/nme/src/CommandLineTools.hx index b27a20cb0..5f7cad834 100644 --- a/tools/nme/src/CommandLineTools.hx +++ b/tools/nme/src/CommandLineTools.hx @@ -64,7 +64,7 @@ class CommandLineTools "installer", "copy-if-newer", "tidy", "set", "unset", "nocompile", "clean", "update", "build", "run", "rerun", "install", "uninstall", "trace", "test", "rebuild", "shell", "icon", "banner", "favicon", "serve", "listbrowsers", - "prepare", "quickrun", "uploadcrashlytics", "ndk-stack" ]; + "prepare", "quickrun", "uploadcrashlytics", "ndk-stack", "emulator" ]; static var setNames = [ "target", "bin", "command", "cppiaHost", "cppiaClassPath", "deploy", "developmentTeam" ]; static var setNamesHelp = [ "default when no target is specifiec", "alternate location for binary files", "default command to run", "executable for running cppia code", "additional class path when building cppia", "remote deployment host", "IOS development team id (10 character code)" ]; static var quickSetNames = [ "debug", "verbose" ]; @@ -789,6 +789,7 @@ class CommandLineTools sys.println(" demo : Run an existing sample or project"); sys.println(" create : Create a new project or extension using templates"); sys.println(" setup : Create an alias for nme so you don't need to type 'haxelib run nme...'"); + sys.println(" emulator : start/list an android emulator"); sys.println(" rebuild : rebuild binaries from a build.xml file'"); sys.println(" remake : rebuild nme tool and build nme project binaries for targets'"); sys.println(" listbrowsers: show list of browsers that can be used with js targets"); @@ -1462,6 +1463,10 @@ class CommandLineTools case "generate": generate(); + case "emulator": + var android = new platforms.AndroidPlatform(project); + android.emulator(words); + case "clone": processSample(project,"clone"); diff --git a/tools/nme/src/platforms/AndroidPlatform.hx b/tools/nme/src/platforms/AndroidPlatform.hx index 442c39fc0..10d79edc3 100644 --- a/tools/nme/src/platforms/AndroidPlatform.hx +++ b/tools/nme/src/platforms/AndroidPlatform.hx @@ -84,7 +84,18 @@ class AndroidPlatform extends Platform project.androidConfig.ABIs = [abi]; if (project.androidConfig.ABIs.length==0) + { + var emulators = getAvdList(); + if (emulators==null || emulators.length==0) + { + Log.info(" Could not find any emulators - try installing from Android Studio"); + } + else + { + Log.info(" try starting one of the emulators with emulator emulator_name from" + emulators); + } Log.error("Could not determine build target from adb, and no test ABI specified"); + } } else if(project.androidConfig.ABIs.length == 0) { @@ -130,6 +141,25 @@ class AndroidPlatform extends Platform } } + public function emulator(args:Array) + { + if (args.length!=1) + { + Log.error("Usage: name emulator [emulator_name|list]"); + } + if (args[0]=="list") + { + var emus = getAvdList(); + Sys.println('Emulators: $emus'); + } + else + { + var emu = getEmulatorExe(); + if (emu==null) + Log.error("Could not find emulator exe."); + ProcessHelper.runCommand("", emu, [ "-avd", args[0] ] ); + } + } function findArchitectureByName(arch:String) : Architecture { @@ -349,6 +379,37 @@ class AndroidPlatform extends Platform return exe; } + static var emulatorExe:String = null; + function getEmulatorExe() : String + { + if (emulatorExe==null) + { + var ext = ""; + if (PlatformHelper.hostPlatform==Platform.WINDOWS) + ext = ".exe"; + var test = project.environment.get("ANDROID_SDK") + "/emulator/emulator" + ext; + if (FileSystem.exists(test)) + { + Log.verbose("Found emulator exe at:" + test); + emulatorExe = test; + } + else + emulatorExe = ""; + } + return emulatorExe=="" ? null : emulatorExe; + } + + + public function getAvdList() : Array + { + var exe = getEmulatorExe(); + if (exe==null) + return null; + + var out = ProcessHelper.getOutput(exe, [ "-list-avds"]); + return out; + } + public function runNdkStack(args:Array) { var exe = getNdkStackExe(); @@ -530,7 +591,7 @@ class AndroidPlatform extends Platform private function queryDeviceABI():String { var lines = ProcessHelper.getOutput(adbName,"shell getprop ro.product.cpu.abi".split(' '), Log.mVerbose); - trace(lines); + //trace(lines); if(lines.length > 0) { if(lines[0].indexOf('error') == -1) { var abi = lines[0].split("\r")[0];