From 99173c5fc4820e9f608d3753ddea9819fd22304d Mon Sep 17 00:00:00 2001 From: fhuya Date: Tue, 17 Mar 2020 18:16:49 -0700 Subject: Complete the implementation of the GodotPayment plugin. Move the remaining plugin components within the plugin source code. --- .../vending/billing/IInAppBillingService.aidl | 281 -------------- .../java/lib/src/org/godotengine/godot/Godot.java | 18 +- .../godotengine/godot/payments/ConsumeTask.java | 116 ------ .../godot/payments/GodotPaymentInterface.java | 97 ----- .../godot/payments/HandlePurchaseTask.java | 93 ----- .../godotengine/godot/payments/PaymentsCache.java | 72 ---- .../godot/payments/PaymentsManager.java | 418 --------------------- .../godotengine/godot/payments/PurchaseTask.java | 118 ------ .../godot/payments/ReleaseAllConsumablesTask.java | 141 ------- .../godotengine/godot/payments/ValidateTask.java | 141 ------- .../vending/billing/IInAppBillingService.aidl | 281 ++++++++++++++ .../godot/plugin/payment/ConsumeTask.java | 116 ++++++ .../godot/plugin/payment/GodotPayment.java | 59 ++- .../godot/plugin/payment/HandlePurchaseTask.java | 93 +++++ .../godot/plugin/payment/PaymentsCache.java | 71 ++++ .../godot/plugin/payment/PaymentsManager.java | 405 ++++++++++++++++++++ .../godot/plugin/payment/PurchaseTask.java | 118 ++++++ .../plugin/payment/ReleaseAllConsumablesTask.java | 141 +++++++ .../godot/plugin/payment/ValidateTask.java | 141 +++++++ 19 files changed, 1392 insertions(+), 1528 deletions(-) delete mode 100644 platform/android/java/lib/aidl/com/android/vending/billing/IInAppBillingService.aidl delete mode 100644 platform/android/java/lib/src/org/godotengine/godot/payments/ConsumeTask.java delete mode 100644 platform/android/java/lib/src/org/godotengine/godot/payments/GodotPaymentInterface.java delete mode 100644 platform/android/java/lib/src/org/godotengine/godot/payments/HandlePurchaseTask.java delete mode 100644 platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsCache.java delete mode 100644 platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java delete mode 100644 platform/android/java/lib/src/org/godotengine/godot/payments/PurchaseTask.java delete mode 100644 platform/android/java/lib/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java delete mode 100644 platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java create mode 100644 platform/android/java/plugins/godotpayment/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl create mode 100644 platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ConsumeTask.java create mode 100644 platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/HandlePurchaseTask.java create mode 100644 platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PaymentsCache.java create mode 100644 platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PaymentsManager.java create mode 100644 platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PurchaseTask.java create mode 100644 platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ReleaseAllConsumablesTask.java create mode 100644 platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ValidateTask.java (limited to 'platform/android/java') diff --git a/platform/android/java/lib/aidl/com/android/vending/billing/IInAppBillingService.aidl b/platform/android/java/lib/aidl/com/android/vending/billing/IInAppBillingService.aidl deleted file mode 100644 index 0f2bcae338..0000000000 --- a/platform/android/java/lib/aidl/com/android/vending/billing/IInAppBillingService.aidl +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.vending.billing; - -import android.os.Bundle; - -/** - * 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_SERVICE_UNAVAILABLE = 2 - The network connection is down - * 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 - */ -interface IInAppBillingService { - /** - * Checks support for the requested billing API version, package and in-app type. - * Minimum API version supported by this interface is 3. - * @param apiVersion billing API version that 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 subscriptions) - * @return RESULT_OK(0) on success and appropriate response code on failures. - */ - int isBillingSupported(int apiVersion, String packageName, String type); - - /** - * 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 app is using - * @param packageName the package name of the calling app - * @param type of the in-app items ("inapp" for one-time purchases - * and "subs" for subscriptions) - * @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, appropriate response codes - * on failures. - * "DETAILS_LIST" with a StringArrayList containing purchase information - * in JSON format similar to: - * '{ "productId" : "exampleSku", - * "type" : "inapp", - * "price" : "$5.00", - * "price_currency": "USD", - * "price_amount_micros": 5000000, - * "title : "Example Title", - * "description" : "This is an example description" }' - */ - Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle); - - /** - * 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 of the in-app item being purchased ("inapp" for one-time purchases - * and "subs" for subscriptions) - * @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, appropriate response codes - * on failures. - * "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, appropriate response - * codes on failures. - * "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 - */ - Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, - String developerPayload); - - /** - * 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 of the in-app items being requested ("inapp" for one-time purchases - * and "subs" for subscriptions) - * @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, appropriate response codes - on failures. - * "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. - */ - Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken); - - /** - * 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 RESULT_OK(0) if consumption succeeded, appropriate response codes on failures. - */ - int consumePurchase(int apiVersion, String packageName, String purchaseToken); - - /** - * This API is currently under development. - */ - int stub(int apiVersion, String packageName, String type); - - /** - * Returns a pending intent to launch the purchase flow for upgrading or downgrading a - * subscription. The existing owned SKU(s) should be provided along with the new SKU that - * the user is upgrading or downgrading to. - * @param apiVersion billing API version that the app is using, must be 5 or later - * @param packageName package name of the calling app - * @param oldSkus the SKU(s) that the user is upgrading or downgrading from, - * if null or empty this method will behave like {@link #getBuyIntent} - * @param newSku the SKU that the user is upgrading or downgrading to - * @param type of the item being purchased, currently must be "subs" - * @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, appropriate response codes - * on failures. - * "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, appropriate response - * codes on failures. - * "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 - */ - Bundle getBuyIntentToReplaceSkus(int apiVersion, String packageName, - in List oldSkus, String newSku, String type, String developerPayload); - - /** - * Returns a pending intent to launch the purchase flow for an in-app item. This method is - * a variant of the {@link #getBuyIntent} method and takes an additional {@code extraParams} - * parameter. This parameter is a Bundle of optional keys and values that affect the - * operation of the method. - * @param apiVersion billing API version that the app is using, must be 6 or later - * @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 of the in-app item being purchased ("inapp" for one-time purchases - * and "subs" for subscriptions) - * @param developerPayload optional argument to be sent back with the purchase information - * @extraParams a Bundle with the following optional keys: - * "skusToReplace" - List - an optional list of SKUs that the user is - * upgrading or downgrading from. - * Pass this field if the purchase is upgrading or downgrading - * existing subscriptions. - * The specified SKUs are replaced with the SKUs that the user is - * purchasing. Google Play replaces the specified SKUs at the start of - * the next billing cycle. - * "replaceSkusProration" - Boolean - whether the user should be credited for any unused - * subscription time on the SKUs they are upgrading or downgrading. - * If you set this field to true, Google Play swaps out the old SKUs - * and credits the user with the unused value of their subscription - * time on a pro-rated basis. - * Google Play applies this credit to the new subscription, and does - * not begin billing the user for the new subscription until after - * the credit is used up. - * If you set this field to false, the user does not receive credit for - * any unused subscription time and the recurrence date does not - * change. - * Default value is true. Ignored if you do not pass skusToReplace. - * "accountId" - String - an optional obfuscated string that is uniquely - * associated with the user's account in your app. - * If you pass this value, Google Play can use it to detect irregular - * activity, such as many devices making purchases on the same - * account in a short period of time. - * Do not use the developer ID or the user's Google ID for this field. - * In addition, this field should not contain the user's ID in - * cleartext. - * We recommend that you use a one-way hash to generate a string from - * the user's ID, and store the hashed string in this field. - * "vr" - Boolean - an optional flag indicating whether the returned intent - * should start a VR purchase flow. The apiVersion must also be 7 or - * later to use this flag. - */ - Bundle getBuyIntentExtraParams(int apiVersion, String packageName, String sku, - String type, String developerPayload, in Bundle extraParams); - - /** - * Returns the most recent purchase made by the user for each SKU, even if that purchase is - * expired, canceled, or consumed. - * @param apiVersion billing API version that the app is using, must be 6 or later - * @param packageName package name of the calling app - * @param type of the in-app items being requested ("inapp" for one-time purchases - * and "subs" for subscriptions) - * @param continuationToken to be set as null for the first call, if the number of owned - * skus is too large, 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. - * @param extraParams a Bundle with extra params that would be appended into http request - * query string. Not used at this moment. Reserved for future functionality. - * @return Bundle containing the following key-value pairs - * "RESPONSE_CODE" with int value: RESULT_OK(0) if success, - * {@link IabHelper#BILLING_RESPONSE_RESULT_*} response codes on failures. - * - * "INAPP_PURCHASE_ITEM_LIST" - ArrayList containing the list of SKUs - * "INAPP_PURCHASE_DATA_LIST" - ArrayList containing the purchase information - * "INAPP_DATA_SIGNATURE_LIST"- ArrayList 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. - */ - Bundle getPurchaseHistory(int apiVersion, String packageName, String type, - String continuationToken, in Bundle extraParams); - - /** - * This method is a variant of {@link #isBillingSupported}} that takes an additional - * {@code extraParams} parameter. - * @param apiVersion billing API version that the app is using, must be 7 or later - * @param packageName package name of the calling app - * @param type of the in-app item being purchased ("inapp" for one-time purchases and "subs" - * for subscriptions) - * @param extraParams a Bundle with the following optional keys: - * "vr" - Boolean - an optional flag to indicate whether {link #getBuyIntentExtraParams} - * supports returning a VR purchase flow. - * @return RESULT_OK(0) on success and appropriate response code on failures. - */ - int isBillingSupportedExtraParams(int apiVersion, String packageName, String type, - in Bundle extraParams); -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java index 7db4aa6597..4e605f9950 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -95,7 +95,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Locale; import org.godotengine.godot.input.GodotEditText; -import org.godotengine.godot.payments.PaymentsManager; import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.plugin.GodotPluginRegistry; import org.godotengine.godot.utils.GodotNetUtils; @@ -174,21 +173,17 @@ public abstract class Godot extends FragmentActivity implements SensorEventListe } public ResultCallback result_callback; - private PaymentsManager mPaymentsManager = null; - @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == PaymentsManager.REQUEST_CODE_FOR_PURCHASE) { - mPaymentsManager.processPurchaseResponse(resultCode, data); - } else if (result_callback != null) { + if (result_callback != null) { result_callback.callback(requestCode, resultCode, data); result_callback = null; - }; + } for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { plugin.onMainActivityResult(requestCode, resultCode, data); } - }; + } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { @@ -445,8 +440,6 @@ public abstract class Godot extends FragmentActivity implements SensorEventListe result_callback = null; - mPaymentsManager = PaymentsManager.createManager(this).initService(); - godot_initialized = true; } @@ -603,7 +596,6 @@ public abstract class Godot extends FragmentActivity implements SensorEventListe @Override protected void onDestroy() { - if (mPaymentsManager != null) mPaymentsManager.destroy(); for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { plugin.onMainDestroy(); } @@ -938,10 +930,6 @@ public abstract class Godot extends FragmentActivity implements SensorEventListe return true; } - public PaymentsManager getPaymentsManager() { - return mPaymentsManager; - } - public boolean requestPermission(String p_name) { return PermissionsUtil.requestPermission(p_name, this); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/ConsumeTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/ConsumeTask.java deleted file mode 100644 index 95cc48f536..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/payments/ConsumeTask.java +++ /dev/null @@ -1,116 +0,0 @@ -/*************************************************************************/ -/* ConsumeTask.java */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -package org.godotengine.godot.payments; - -import android.content.Context; -import android.os.AsyncTask; -import android.os.RemoteException; -import com.android.vending.billing.IInAppBillingService; -import java.lang.ref.WeakReference; - -abstract public class ConsumeTask { - - private Context context; - private IInAppBillingService mService; - - private String mSku; - private String mToken; - - private static class ConsumeAsyncTask extends AsyncTask { - - private WeakReference mTask; - - ConsumeAsyncTask(ConsumeTask consume) { - mTask = new WeakReference<>(consume); - } - - @Override - protected String doInBackground(String... strings) { - ConsumeTask consume = mTask.get(); - if (consume != null) { - return consume.doInBackground(strings); - } - return null; - } - - @Override - protected void onPostExecute(String param) { - ConsumeTask consume = mTask.get(); - if (consume != null) { - consume.onPostExecute(param); - } - } - } - - public ConsumeTask(IInAppBillingService mService, Context context) { - this.context = context; - this.mService = mService; - } - - public void consume(final String sku) { - mSku = sku; - PaymentsCache pc = new PaymentsCache(context); - Boolean isBlocked = pc.getConsumableFlag("block", sku); - mToken = pc.getConsumableValue("token", sku); - if (!isBlocked && mToken == null) { - // Consuming task is processing - } else if (!isBlocked) { - return; - } else if (mToken == null) { - this.error("No token for sku:" + sku); - return; - } - new ConsumeAsyncTask(this).execute(); - } - - private String doInBackground(String... params) { - try { - int response = mService.consumePurchase(3, context.getPackageName(), mToken); - if (response == 0 || response == 8) { - return null; - } - } catch (RemoteException e) { - return e.getMessage(); - } - return "Some error"; - } - - private void onPostExecute(String param) { - if (param == null) { - success(new PaymentsCache(context).getConsumableValue("ticket", mSku)); - } else { - error(param); - } - } - - abstract protected void success(String ticket); - abstract protected void error(String message); -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/GodotPaymentInterface.java b/platform/android/java/lib/src/org/godotengine/godot/payments/GodotPaymentInterface.java deleted file mode 100644 index 6ac7338b30..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/payments/GodotPaymentInterface.java +++ /dev/null @@ -1,97 +0,0 @@ -/*************************************************************************/ -/* GodotPaymentInterface.java */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -package org.godotengine.godot.payments; - -public interface GodotPaymentInterface { - void purchase(String sku, String transactionId); - - void consumeUnconsumedPurchases(); - - String getSignature(); - - void callbackSuccess(String ticket, String signature, String sku); - - void callbackSuccessProductMassConsumed(String ticket, String signature, String sku); - - void callbackSuccessNoUnconsumedPurchases(); - - void callbackFailConsume(String message); - - void callbackFail(String message); - - void callbackCancel(); - - void callbackAlreadyOwned(String sku); - - int getPurchaseCallbackId(); - - void setPurchaseCallbackId(int purchaseCallbackId); - - String getPurchaseValidationUrlPrefix(); - - void setPurchaseValidationUrlPrefix(String url); - - String getAccessToken(); - - void setAccessToken(String accessToken); - - void setTransactionId(String transactionId); - - String getTransactionId(); - - // request purchased items are not consumed - void requestPurchased(); - - // callback for requestPurchased() - void callbackPurchased(String receipt, String signature, String sku); - - void callbackDisconnected(); - - void callbackConnected(); - - // true if connected, false otherwise - boolean isConnected(); - - // consume item automatically after purchase. default is true. - void setAutoConsume(boolean autoConsume); - - // consume a specific item - void consume(String sku); - - // query in app item detail info - void querySkuDetails(String[] list); - - void addSkuDetail(String itemJson); - - void completeSkuDetail(); - - void errorSkuDetail(String errorMessage); -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/HandlePurchaseTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/HandlePurchaseTask.java deleted file mode 100644 index 23d693cc8c..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/payments/HandlePurchaseTask.java +++ /dev/null @@ -1,93 +0,0 @@ -/*************************************************************************/ -/* HandlePurchaseTask.java */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -package org.godotengine.godot.payments; - -import android.app.Activity; -import android.content.Intent; -import org.json.JSONException; -import org.json.JSONObject; - -abstract public class HandlePurchaseTask { - - private Activity context; - - public HandlePurchaseTask(Activity context) { - this.context = context; - } - - public void handlePurchaseRequest(int resultCode, Intent data) { - //Log.d("XXX", "Handling purchase response"); - if (resultCode == Activity.RESULT_OK) { - try { - //int responseCode = data.getIntExtra("RESPONSE_CODE", 0); - PaymentsCache pc = new PaymentsCache(context); - - String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA"); - //Log.d("XXX", "Purchase data:" + purchaseData); - String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE"); - //Log.d("XXX", "Purchase signature:" + dataSignature); - //Log.d("SARLANGA", purchaseData); - - JSONObject jo = new JSONObject(purchaseData); - //String sku = jo.getString("productId"); - //alert("You have bought the " + sku + ". Excellent choice, aventurer!"); - //String orderId = jo.getString("orderId"); - //String packageName = jo.getString("packageName"); - String productId = jo.getString("productId"); - //Long purchaseTime = jo.getLong("purchaseTime"); - //Integer state = jo.getInt("purchaseState"); - String developerPayload = jo.getString("developerPayload"); - String purchaseToken = jo.getString("purchaseToken"); - - if (!pc.getConsumableValue("validation_hash", productId).equals(developerPayload)) { - error("Untrusted callback"); - return; - } - //Log.d("XXX", "Este es el product ID:" + productId); - pc.setConsumableValue("ticket_signautre", productId, dataSignature); - pc.setConsumableValue("ticket", productId, purchaseData); - pc.setConsumableFlag("block", productId, true); - pc.setConsumableValue("token", productId, purchaseToken); - - success(productId, dataSignature, purchaseData); - return; - } catch (JSONException e) { - error(e.getMessage()); - } - } else if (resultCode == Activity.RESULT_CANCELED) { - canceled(); - } - } - - abstract protected void success(String sku, String signature, String ticket); - abstract protected void error(String message); - abstract protected void canceled(); -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsCache.java b/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsCache.java deleted file mode 100644 index 84a7eda6e0..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsCache.java +++ /dev/null @@ -1,72 +0,0 @@ -/*************************************************************************/ -/* PaymentsCache.java */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -package org.godotengine.godot.payments; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; - -public class PaymentsCache { - - public Context context; - - public PaymentsCache(Context context) { - this.context = context; - } - - public void setConsumableFlag(String set, String sku, Boolean flag) { - SharedPreferences sharedPref = context.getSharedPreferences("consumables_" + set, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sharedPref.edit(); - editor.putBoolean(sku, flag); - editor.apply(); - } - - public boolean getConsumableFlag(String set, String sku) { - SharedPreferences sharedPref = context.getSharedPreferences( - "consumables_" + set, Context.MODE_PRIVATE); - return sharedPref.getBoolean(sku, false); - } - - public void setConsumableValue(String set, String sku, String value) { - SharedPreferences sharedPref = context.getSharedPreferences("consumables_" + set, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sharedPref.edit(); - editor.putString(sku, value); - //Log.d("XXX", "Setting asset: consumables_" + set + ":" + sku); - editor.apply(); - } - - public String getConsumableValue(String set, String sku) { - SharedPreferences sharedPref = context.getSharedPreferences( - "consumables_" + set, Context.MODE_PRIVATE); - //Log.d("XXX", "Getting asset: consumables_" + set + ":" + sku); - return sharedPref.getString(sku, null); - } -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java b/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java deleted file mode 100644 index 9bf6650f84..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java +++ /dev/null @@ -1,418 +0,0 @@ -/*************************************************************************/ -/* PaymentsManager.java */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -package org.godotengine.godot.payments; - -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 com.android.vending.billing.IInAppBillingService; -import java.util.ArrayList; -import java.util.Arrays; -import org.json.JSONException; -import org.json.JSONObject; - -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) { - this.activity = activity; - } - - public static PaymentsManager createManager(Activity activity) { - PaymentsManager manager = new PaymentsManager(activity); - return manager; - } - - private PaymentsManager(Activity activity) { - this.activity = activity; - } - - public PaymentsManager initService() { - Intent intent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); - intent.setPackage("com.android.vending"); - activity.bindService( - intent, - mServiceConn, - Context.BIND_AUTO_CREATE); - return this; - } - - public void destroy() { - if (mService != null) { - activity.unbindService(mServiceConn); - } - } - - ServiceConnection mServiceConn = new ServiceConnection() { - @Override - public void onServiceDisconnected(ComponentName name) { - mService = null; - - // At this stage, godotPayment might not have been initialized yet. - if (godotPayment != null) { - godotPayment.callbackDisconnected(); - } - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - mService = IInAppBillingService.Stub.asInterface(service); - - // At this stage, godotPayment might not have been initialized yet. - if (godotPayment != null) { - godotPayment.callbackConnected(); - } - } - }; - - public void requestPurchase(final String sku, String transactionId) { - new PurchaseTask(mService, activity) { - @Override - protected void error(String message) { - godotPayment.callbackFail(message); - } - - @Override - protected void canceled() { - godotPayment.callbackCancel(); - } - - @Override - protected void alreadyOwned() { - godotPayment.callbackAlreadyOwned(sku); - } - } - .purchase(sku, transactionId); - } - - public boolean isConnected() { - return mService != null; - } - - public void consumeUnconsumedPurchases() { - new ReleaseAllConsumablesTask(mService, activity) { - @Override - protected void success(String sku, String receipt, String signature, String token) { - godotPayment.callbackSuccessProductMassConsumed(receipt, signature, sku); - } - - @Override - protected void error(String message) { - Log.d("godot", "consumeUnconsumedPurchases :" + message); - godotPayment.callbackFailConsume(message); - } - - @Override - protected void notRequired() { - Log.d("godot", "callbackSuccessNoUnconsumedPurchases :"); - godotPayment.callbackSuccessNoUnconsumedPurchases(); - } - } - .consumeItAll(); - } - - public void requestPurchased() { - try { - PaymentsCache pc = new PaymentsCache(activity); - - String continueToken = null; - - do { - Bundle bundle = mService.getPurchases(3, activity.getPackageName(), "inapp", continueToken); - - if (bundle.getInt("RESPONSE_CODE") == 0) { - - final ArrayList myPurchases = bundle.getStringArrayList("INAPP_PURCHASE_DATA_LIST"); - final ArrayList mySignatures = bundle.getStringArrayList("INAPP_DATA_SIGNATURE_LIST"); - - if (myPurchases == null || myPurchases.size() == 0) { - godotPayment.callbackPurchased("", "", ""); - return; - } - - 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); - - godotPayment.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()); - } - } - - public void processPurchaseResponse(int resultCode, Intent data) { - new HandlePurchaseTask(activity) { - @Override - protected void success(final String sku, final String signature, final String ticket) { - godotPayment.callbackSuccess(ticket, signature, sku); - - if (auto_consume) { - new ConsumeTask(mService, activity) { - @Override - protected void success(String ticket) { - } - - @Override - protected void error(String message) { - godotPayment.callbackFail(message); - } - } - .consume(sku); - } - } - - @Override - protected void error(String message) { - godotPayment.callbackFail(message); - } - - @Override - protected void canceled() { - godotPayment.callbackCancel(); - } - } - .handlePurchaseRequest(resultCode, data); - } - - public void validatePurchase(String purchaseToken, final String sku) { - - new ValidateTask(activity, godotPayment) { - @Override - protected void success() { - - new ConsumeTask(mService, activity) { - @Override - protected void success(String ticket) { - godotPayment.callbackSuccess(ticket, null, sku); - } - - @Override - protected void error(String message) { - godotPayment.callbackFail(message); - } - } - .consume(sku); - } - - @Override - protected void error(String message) { - godotPayment.callbackFail(message); - } - - @Override - protected void canceled() { - godotPayment.callbackCancel(); - } - } - .validatePurchase(sku); - } - - public void setAutoConsume(boolean autoConsume) { - auto_consume = autoConsume; - } - - public void consume(final String sku) { - new ConsumeTask(mService, activity) { - @Override - protected void success(String ticket) { - godotPayment.callbackSuccessProductMassConsumed(ticket, "", sku); - } - - @Override - protected void error(String message) { - godotPayment.callbackFailConsume(message); - } - } - .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) { - godotPayment.errorSkuDetail(getResponseDesc(response)); - } else { - godotPayment.errorSkuDetail("No error but no detail list."); - } - return; - } - - ArrayList responseList = skuDetails.getStringArrayList("DETAILS_LIST"); - - for (String thisResponse : responseList) { - Log.d("godot", "response = " + thisResponse); - godotPayment.addSkuDetail(thisResponse); - } - } catch (RemoteException e) { - e.printStackTrace(); - godotPayment.errorSkuDetail("RemoteException error!"); - } - } - godotPayment.completeSkuDetail(); - } - })) - .start(); - } - - private GodotPaymentInterface godotPayment; - - public void setBaseSingleton(GodotPaymentInterface godotPaymentInterface) { - this.godotPayment = godotPaymentInterface; - } -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/PurchaseTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/PurchaseTask.java deleted file mode 100644 index 09c9349124..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/payments/PurchaseTask.java +++ /dev/null @@ -1,118 +0,0 @@ -/*************************************************************************/ -/* PurchaseTask.java */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -package org.godotengine.godot.payments; - -import android.app.Activity; -import android.app.PendingIntent; -import android.content.Intent; -import android.content.IntentSender.SendIntentException; -import android.os.Bundle; -import android.os.RemoteException; -import android.util.Log; -import com.android.vending.billing.IInAppBillingService; - -abstract public class PurchaseTask { - - private Activity context; - - private IInAppBillingService mService; - public PurchaseTask(IInAppBillingService mService, Activity context) { - this.context = context; - this.mService = mService; - } - - private boolean isLooping = false; - - public void purchase(final String sku, final String transactionId) { - Log.d("XXX", "Starting purchase for: " + sku); - PaymentsCache pc = new PaymentsCache(context); - Boolean isBlocked = pc.getConsumableFlag("block", sku); - /* - if(isBlocked){ - Log.d("XXX", "Is awaiting payment confirmation"); - error("Awaiting payment confirmation"); - return; - } - */ - final String hash = transactionId; - - Bundle buyIntentBundle; - try { - buyIntentBundle = mService.getBuyIntent(3, context.getApplicationContext().getPackageName(), sku, "inapp", hash); - } catch (RemoteException e) { - //Log.d("XXX", "Error: " + e.getMessage()); - error(e.getMessage()); - return; - } - Object rc = buyIntentBundle.get("RESPONSE_CODE"); - int responseCode = 0; - if (rc == null) { - responseCode = PaymentsManager.BILLING_RESPONSE_RESULT_OK; - } else if (rc instanceof Integer) { - responseCode = ((Integer)rc).intValue(); - } else if (rc instanceof Long) { - responseCode = (int)((Long)rc).longValue(); - } - //Log.d("XXX", "Buy intent response code: " + responseCode); - if (responseCode == 1 || responseCode == 3 || responseCode == 4) { - canceled(); - return; - } - if (responseCode == 7) { - alreadyOwned(); - return; - } - - PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT"); - pc.setConsumableValue("validation_hash", sku, hash); - try { - if (context == null) { - //Log.d("XXX", "No context!"); - } - if (pendingIntent == null) { - //Log.d("XXX", "No pending intent"); - } - //Log.d("XXX", "Starting activity for purchase!"); - context.startIntentSenderForResult( - pendingIntent.getIntentSender(), - PaymentsManager.REQUEST_CODE_FOR_PURCHASE, - new Intent(), - Integer.valueOf(0), Integer.valueOf(0), - Integer.valueOf(0)); - } catch (SendIntentException e) { - error(e.getMessage()); - } - } - - abstract protected void error(String message); - abstract protected void canceled(); - abstract protected void alreadyOwned(); -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java deleted file mode 100644 index a101780511..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java +++ /dev/null @@ -1,141 +0,0 @@ -/*************************************************************************/ -/* ReleaseAllConsumablesTask.java */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -package org.godotengine.godot.payments; - -import android.content.Context; -import android.os.AsyncTask; -import android.os.Bundle; -import android.util.Log; -import com.android.vending.billing.IInAppBillingService; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import org.json.JSONException; -import org.json.JSONObject; - -abstract public class ReleaseAllConsumablesTask { - - private Context context; - private IInAppBillingService mService; - - private static class ReleaseAllConsumablesAsyncTask extends AsyncTask { - - private WeakReference mTask; - private String mSku; - private String mReceipt; - private String mSignature; - private String mToken; - - ReleaseAllConsumablesAsyncTask(ReleaseAllConsumablesTask task, String sku, String receipt, String signature, String token) { - mTask = new WeakReference(task); - - mSku = sku; - mReceipt = receipt; - mSignature = signature; - mToken = token; - } - - @Override - protected String doInBackground(String... params) { - ReleaseAllConsumablesTask consume = mTask.get(); - if (consume != null) { - return consume.doInBackground(mToken); - } - return null; - } - - @Override - protected void onPostExecute(String param) { - ReleaseAllConsumablesTask consume = mTask.get(); - if (consume != null) { - consume.success(mSku, mReceipt, mSignature, mToken); - } - } - } - - public ReleaseAllConsumablesTask(IInAppBillingService mService, Context context) { - this.context = context; - this.mService = mService; - } - - public void consumeItAll() { - try { - //Log.d("godot", "consumeItall for " + context.getPackageName()); - Bundle bundle = mService.getPurchases(3, context.getPackageName(), "inapp", null); - - if (bundle.getInt("RESPONSE_CODE") == 0) { - - final ArrayList myPurchases = bundle.getStringArrayList("INAPP_PURCHASE_DATA_LIST"); - final ArrayList mySignatures = bundle.getStringArrayList("INAPP_DATA_SIGNATURE_LIST"); - - if (myPurchases == null || myPurchases.size() == 0) { - //Log.d("godot", "No purchases!"); - notRequired(); - return; - } - - //Log.d("godot", "# products to be consumed:" + myPurchases.size()); - 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); - //Log.d("godot", "A punto de consumir un item con token:" + token + "\n" + receipt); - new ReleaseAllConsumablesAsyncTask(this, sku, receipt, signature, token).execute(); - } catch (JSONException e) { - } - } - } - } catch (Exception e) { - Log.d("godot", "Error releasing products:" + e.getClass().getName() + ":" + e.getMessage()); - } - } - - private String doInBackground(String token) { - try { - //Log.d("godot", "Requesting to consume an item with token ." + token); - int response = mService.consumePurchase(3, context.getPackageName(), token); - //Log.d("godot", "consumePurchase response: " + response); - if (response == 0 || response == 8) { - return null; - } - } catch (Exception e) { - Log.d("godot", "Error " + e.getClass().getName() + ":" + e.getMessage()); - } - return null; - } - - abstract protected void success(String sku, String receipt, String signature, String token); - abstract protected void error(String message); - abstract protected void notRequired(); -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java deleted file mode 100644 index 10c314aecf..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java +++ /dev/null @@ -1,141 +0,0 @@ -/*************************************************************************/ -/* ValidateTask.java */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -package org.godotengine.godot.payments; - -import android.app.Activity; -import android.app.ProgressDialog; -import android.os.AsyncTask; -import java.lang.ref.WeakReference; -import org.godotengine.godot.utils.HttpRequester; -import org.godotengine.godot.utils.RequestParams; -import org.json.JSONException; -import org.json.JSONObject; - -abstract public class ValidateTask { - - private Activity context; - private GodotPaymentInterface godotPayments; - private ProgressDialog dialog; - private String mSku; - - private static class ValidateAsyncTask extends AsyncTask { - private WeakReference mTask; - - ValidateAsyncTask(ValidateTask task) { - mTask = new WeakReference<>(task); - } - - @Override - protected void onPreExecute() { - ValidateTask task = mTask.get(); - if (task != null) { - task.onPreExecute(); - } - } - - @Override - protected String doInBackground(String... params) { - ValidateTask task = mTask.get(); - if (task != null) { - return task.doInBackground(params); - } - return null; - } - - @Override - protected void onPostExecute(String response) { - ValidateTask task = mTask.get(); - if (task != null) { - task.onPostExecute(response); - } - } - } - - public ValidateTask(Activity context, GodotPaymentInterface godotPayments) { - this.context = context; - this.godotPayments = godotPayments; - } - - public void validatePurchase(final String sku) { - mSku = sku; - new ValidateAsyncTask(this).execute(); - } - - private void onPreExecute() { - dialog = ProgressDialog.show(context, null, "Please wait..."); - } - - private String doInBackground(String... params) { - PaymentsCache pc = new PaymentsCache(context); - String url = godotPayments.getPurchaseValidationUrlPrefix(); - RequestParams param = new RequestParams(); - param.setUrl(url); - param.put("ticket", pc.getConsumableValue("ticket", mSku)); - param.put("purchaseToken", pc.getConsumableValue("token", mSku)); - param.put("sku", mSku); - //Log.d("XXX", "Haciendo request a " + url); - //Log.d("XXX", "ticket: " + pc.getConsumableValue("ticket", sku)); - //Log.d("XXX", "purchaseToken: " + pc.getConsumableValue("token", sku)); - //Log.d("XXX", "sku: " + sku); - param.put("package", context.getApplicationContext().getPackageName()); - HttpRequester requester = new HttpRequester(); - String jsonResponse = requester.post(param); - //Log.d("XXX", "Validation response:\n"+jsonResponse); - return jsonResponse; - } - - private void onPostExecute(String response) { - if (dialog != null) { - dialog.dismiss(); - dialog = null; - } - JSONObject j; - try { - j = new JSONObject(response); - if (j.getString("status").equals("OK")) { - success(); - return; - } else if (j.getString("status") != null) { - error(j.getString("message")); - } else { - error("Connection error"); - } - } catch (JSONException e) { - error(e.getMessage()); - } catch (Exception e) { - error(e.getMessage()); - } - } - - abstract protected void success(); - abstract protected void error(String message); - abstract protected void canceled(); -} diff --git a/platform/android/java/plugins/godotpayment/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl b/platform/android/java/plugins/godotpayment/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl new file mode 100644 index 0000000000..0f2bcae338 --- /dev/null +++ b/platform/android/java/plugins/godotpayment/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.vending.billing; + +import android.os.Bundle; + +/** + * 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_SERVICE_UNAVAILABLE = 2 - The network connection is down + * 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 + */ +interface IInAppBillingService { + /** + * Checks support for the requested billing API version, package and in-app type. + * Minimum API version supported by this interface is 3. + * @param apiVersion billing API version that 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 subscriptions) + * @return RESULT_OK(0) on success and appropriate response code on failures. + */ + int isBillingSupported(int apiVersion, String packageName, String type); + + /** + * 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 app is using + * @param packageName the package name of the calling app + * @param type of the in-app items ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @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, appropriate response codes + * on failures. + * "DETAILS_LIST" with a StringArrayList containing purchase information + * in JSON format similar to: + * '{ "productId" : "exampleSku", + * "type" : "inapp", + * "price" : "$5.00", + * "price_currency": "USD", + * "price_amount_micros": 5000000, + * "title : "Example Title", + * "description" : "This is an example description" }' + */ + Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle); + + /** + * 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 of the in-app item being purchased ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @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, appropriate response codes + * on failures. + * "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, appropriate response + * codes on failures. + * "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 + */ + Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, + String developerPayload); + + /** + * 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 of the in-app items being requested ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @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, appropriate response codes + on failures. + * "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. + */ + Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken); + + /** + * 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 RESULT_OK(0) if consumption succeeded, appropriate response codes on failures. + */ + int consumePurchase(int apiVersion, String packageName, String purchaseToken); + + /** + * This API is currently under development. + */ + int stub(int apiVersion, String packageName, String type); + + /** + * Returns a pending intent to launch the purchase flow for upgrading or downgrading a + * subscription. The existing owned SKU(s) should be provided along with the new SKU that + * the user is upgrading or downgrading to. + * @param apiVersion billing API version that the app is using, must be 5 or later + * @param packageName package name of the calling app + * @param oldSkus the SKU(s) that the user is upgrading or downgrading from, + * if null or empty this method will behave like {@link #getBuyIntent} + * @param newSku the SKU that the user is upgrading or downgrading to + * @param type of the item being purchased, currently must be "subs" + * @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, appropriate response codes + * on failures. + * "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, appropriate response + * codes on failures. + * "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 + */ + Bundle getBuyIntentToReplaceSkus(int apiVersion, String packageName, + in List oldSkus, String newSku, String type, String developerPayload); + + /** + * Returns a pending intent to launch the purchase flow for an in-app item. This method is + * a variant of the {@link #getBuyIntent} method and takes an additional {@code extraParams} + * parameter. This parameter is a Bundle of optional keys and values that affect the + * operation of the method. + * @param apiVersion billing API version that the app is using, must be 6 or later + * @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 of the in-app item being purchased ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @param developerPayload optional argument to be sent back with the purchase information + * @extraParams a Bundle with the following optional keys: + * "skusToReplace" - List - an optional list of SKUs that the user is + * upgrading or downgrading from. + * Pass this field if the purchase is upgrading or downgrading + * existing subscriptions. + * The specified SKUs are replaced with the SKUs that the user is + * purchasing. Google Play replaces the specified SKUs at the start of + * the next billing cycle. + * "replaceSkusProration" - Boolean - whether the user should be credited for any unused + * subscription time on the SKUs they are upgrading or downgrading. + * If you set this field to true, Google Play swaps out the old SKUs + * and credits the user with the unused value of their subscription + * time on a pro-rated basis. + * Google Play applies this credit to the new subscription, and does + * not begin billing the user for the new subscription until after + * the credit is used up. + * If you set this field to false, the user does not receive credit for + * any unused subscription time and the recurrence date does not + * change. + * Default value is true. Ignored if you do not pass skusToReplace. + * "accountId" - String - an optional obfuscated string that is uniquely + * associated with the user's account in your app. + * If you pass this value, Google Play can use it to detect irregular + * activity, such as many devices making purchases on the same + * account in a short period of time. + * Do not use the developer ID or the user's Google ID for this field. + * In addition, this field should not contain the user's ID in + * cleartext. + * We recommend that you use a one-way hash to generate a string from + * the user's ID, and store the hashed string in this field. + * "vr" - Boolean - an optional flag indicating whether the returned intent + * should start a VR purchase flow. The apiVersion must also be 7 or + * later to use this flag. + */ + Bundle getBuyIntentExtraParams(int apiVersion, String packageName, String sku, + String type, String developerPayload, in Bundle extraParams); + + /** + * Returns the most recent purchase made by the user for each SKU, even if that purchase is + * expired, canceled, or consumed. + * @param apiVersion billing API version that the app is using, must be 6 or later + * @param packageName package name of the calling app + * @param type of the in-app items being requested ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @param continuationToken to be set as null for the first call, if the number of owned + * skus is too large, 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. + * @param extraParams a Bundle with extra params that would be appended into http request + * query string. Not used at this moment. Reserved for future functionality. + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value: RESULT_OK(0) if success, + * {@link IabHelper#BILLING_RESPONSE_RESULT_*} response codes on failures. + * + * "INAPP_PURCHASE_ITEM_LIST" - ArrayList containing the list of SKUs + * "INAPP_PURCHASE_DATA_LIST" - ArrayList containing the purchase information + * "INAPP_DATA_SIGNATURE_LIST"- ArrayList 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. + */ + Bundle getPurchaseHistory(int apiVersion, String packageName, String type, + String continuationToken, in Bundle extraParams); + + /** + * This method is a variant of {@link #isBillingSupported}} that takes an additional + * {@code extraParams} parameter. + * @param apiVersion billing API version that the app is using, must be 7 or later + * @param packageName package name of the calling app + * @param type of the in-app item being purchased ("inapp" for one-time purchases and "subs" + * for subscriptions) + * @param extraParams a Bundle with the following optional keys: + * "vr" - Boolean - an optional flag to indicate whether {link #getBuyIntentExtraParams} + * supports returning a VR purchase flow. + * @return RESULT_OK(0) on success and appropriate response code on failures. + */ + int isBillingSupportedExtraParams(int apiVersion, String packageName, String type, + in Bundle extraParams); +} diff --git a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ConsumeTask.java b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ConsumeTask.java new file mode 100644 index 0000000000..c15bc232ce --- /dev/null +++ b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ConsumeTask.java @@ -0,0 +1,116 @@ +/*************************************************************************/ +/* ConsumeTask.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +package org.godotengine.godot.plugin.payment; + +import android.content.Context; +import android.os.AsyncTask; +import android.os.RemoteException; +import com.android.vending.billing.IInAppBillingService; +import java.lang.ref.WeakReference; + +abstract public class ConsumeTask { + + private Context context; + private IInAppBillingService mService; + + private String mSku; + private String mToken; + + private static class ConsumeAsyncTask extends AsyncTask { + + private WeakReference mTask; + + ConsumeAsyncTask(ConsumeTask consume) { + mTask = new WeakReference<>(consume); + } + + @Override + protected String doInBackground(String... strings) { + ConsumeTask consume = mTask.get(); + if (consume != null) { + return consume.doInBackground(strings); + } + return null; + } + + @Override + protected void onPostExecute(String param) { + ConsumeTask consume = mTask.get(); + if (consume != null) { + consume.onPostExecute(param); + } + } + } + + public ConsumeTask(IInAppBillingService mService, Context context) { + this.context = context; + this.mService = mService; + } + + public void consume(final String sku) { + mSku = sku; + PaymentsCache pc = new PaymentsCache(context); + Boolean isBlocked = pc.getConsumableFlag("block", sku); + mToken = pc.getConsumableValue("token", sku); + if (!isBlocked && mToken == null) { + // Consuming task is processing + } else if (!isBlocked) { + return; + } else if (mToken == null) { + this.error("No token for sku:" + sku); + return; + } + new ConsumeAsyncTask(this).execute(); + } + + private String doInBackground(String... params) { + try { + int response = mService.consumePurchase(3, context.getPackageName(), mToken); + if (response == 0 || response == 8) { + return null; + } + } catch (RemoteException e) { + return e.getMessage(); + } + return "Some error"; + } + + private void onPostExecute(String param) { + if (param == null) { + success(new PaymentsCache(context).getConsumableValue("ticket", mSku)); + } else { + error(param); + } + } + + abstract protected void success(String ticket); + abstract protected void error(String message); +} diff --git a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/GodotPayment.java b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/GodotPayment.java index 4a6b611c4d..c7d0a5de65 100644 --- a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/GodotPayment.java +++ b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/GodotPayment.java @@ -30,6 +30,7 @@ package org.godotengine.godot.plugin.payment; +import android.content.Intent; import android.support.annotation.NonNull; import android.util.Log; import java.util.ArrayList; @@ -38,28 +39,40 @@ import java.util.List; import org.godotengine.godot.Dictionary; import org.godotengine.godot.Godot; import org.godotengine.godot.GodotLib; -import org.godotengine.godot.payments.GodotPaymentInterface; -import org.godotengine.godot.payments.PaymentsManager; import org.godotengine.godot.plugin.GodotPlugin; import org.json.JSONException; import org.json.JSONObject; -public class GodotPayment extends GodotPlugin implements GodotPaymentInterface { +public class GodotPayment extends GodotPlugin { private Integer purchaseCallbackId = 0; private String accessToken; private String purchaseValidationUrlPrefix; private String transactionId; - private PaymentsManager mPaymentManager; - private Dictionary mSkuDetails = new Dictionary(); + private final PaymentsManager mPaymentManager; + private final Dictionary mSkuDetails = new Dictionary(); public GodotPayment(Godot godot) { super(godot); - mPaymentManager = godot.getPaymentsManager(); - mPaymentManager.setBaseSingleton(this); + mPaymentManager = new PaymentsManager(godot, this); + mPaymentManager.initService(); } @Override + public void onMainActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == PaymentsManager.REQUEST_CODE_FOR_PURCHASE) { + mPaymentManager.processPurchaseResponse(resultCode, data); + } + } + + @Override + public void onMainDestroy() { + super.onMainDestroy(); + if (mPaymentManager != null) { + mPaymentManager.destroy(); + } + } + public void purchase(final String sku, final String transactionId) { runOnUiThread(new Runnable() { @Override @@ -69,7 +82,6 @@ public class GodotPayment extends GodotPlugin implements GodotPaymentInterface { }); } - @Override public void consumeUnconsumedPurchases() { runOnUiThread(new Runnable() { @Override @@ -81,89 +93,72 @@ public class GodotPayment extends GodotPlugin implements GodotPaymentInterface { private String signature; - @Override public String getSignature() { return this.signature; } - @Override public void callbackSuccess(String ticket, String signature, String sku) { GodotLib.calldeferred(purchaseCallbackId, "purchase_success", new Object[] { ticket, signature, sku }); } - @Override 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 }); } - @Override public void callbackSuccessNoUnconsumedPurchases() { GodotLib.calldeferred(purchaseCallbackId, "consume_not_required", new Object[] {}); } - @Override public void callbackFailConsume(String message) { GodotLib.calldeferred(purchaseCallbackId, "consume_fail", new Object[] { message }); } - @Override public void callbackFail(String message) { GodotLib.calldeferred(purchaseCallbackId, "purchase_fail", new Object[] { message }); } - @Override public void callbackCancel() { GodotLib.calldeferred(purchaseCallbackId, "purchase_cancel", new Object[] {}); } - @Override public void callbackAlreadyOwned(String sku) { GodotLib.calldeferred(purchaseCallbackId, "purchase_owned", new Object[] { sku }); } - @Override public int getPurchaseCallbackId() { return purchaseCallbackId; } - @Override public void setPurchaseCallbackId(int purchaseCallbackId) { this.purchaseCallbackId = purchaseCallbackId; } - @Override public String getPurchaseValidationUrlPrefix() { return this.purchaseValidationUrlPrefix; } - @Override public void setPurchaseValidationUrlPrefix(String url) { this.purchaseValidationUrlPrefix = url; } - @Override public String getAccessToken() { return accessToken; } - @Override public void setAccessToken(String accessToken) { this.accessToken = accessToken; } - @Override public void setTransactionId(String transactionId) { this.transactionId = transactionId; } - @Override public String getTransactionId() { return this.transactionId; } // request purchased items are not consumed - @Override public void requestPurchased() { runOnUiThread(new Runnable() { @Override @@ -174,41 +169,34 @@ public class GodotPayment extends GodotPlugin implements GodotPaymentInterface { } // callback for requestPurchased() - @Override public void callbackPurchased(String receipt, String signature, String sku) { GodotLib.calldeferred(purchaseCallbackId, "has_purchased", new Object[] { receipt, signature, sku }); } - @Override public void callbackDisconnected() { GodotLib.calldeferred(purchaseCallbackId, "iap_disconnected", new Object[] {}); } - @Override public void callbackConnected() { GodotLib.calldeferred(purchaseCallbackId, "iap_connected", new Object[] {}); } // true if connected, false otherwise - @Override public boolean isConnected() { return mPaymentManager.isConnected(); } // consume item automatically after purchase. default is true. - @Override public void setAutoConsume(boolean autoConsume) { mPaymentManager.setAutoConsume(autoConsume); } // consume a specific item - @Override public void consume(String sku) { mPaymentManager.consume(sku); } // query in app item detail info - @Override public void querySkuDetails(String[] list) { List nKeys = Arrays.asList(list); List cKeys = Arrays.asList(mSkuDetails.get_keys()); @@ -225,7 +213,6 @@ public class GodotPayment extends GodotPlugin implements GodotPaymentInterface { } } - @Override public void addSkuDetail(String itemJson) { JSONObject o = null; try { @@ -244,12 +231,10 @@ public class GodotPayment extends GodotPlugin implements GodotPaymentInterface { } } - @Override public void completeSkuDetail() { GodotLib.calldeferred(purchaseCallbackId, "sku_details_complete", new Object[] { mSkuDetails }); } - @Override public void errorSkuDetail(String errorMessage) { GodotLib.calldeferred(purchaseCallbackId, "sku_details_error", new Object[] { errorMessage }); } @@ -263,6 +248,8 @@ public class GodotPayment extends GodotPlugin implements GodotPaymentInterface { @NonNull @Override public List getPluginMethods() { - return Arrays.asList("purchase", "setPurchaseCallbackId", "setPurchaseValidationUrlPrefix", "setTransactionId", "getSignature", "consumeUnconsumedPurchases", "requestPurchased", "setAutoConsume", "consume", "querySkuDetails", "isConnected"); + return Arrays.asList("purchase", "setPurchaseCallbackId", "setPurchaseValidationUrlPrefix", + "setTransactionId", "getSignature", "consumeUnconsumedPurchases", "requestPurchased", + "setAutoConsume", "consume", "querySkuDetails", "isConnected"); } } diff --git a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/HandlePurchaseTask.java b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/HandlePurchaseTask.java new file mode 100644 index 0000000000..fe5685288b --- /dev/null +++ b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/HandlePurchaseTask.java @@ -0,0 +1,93 @@ +/*************************************************************************/ +/* HandlePurchaseTask.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +package org.godotengine.godot.plugin.payment; + +import android.app.Activity; +import android.content.Intent; +import org.json.JSONException; +import org.json.JSONObject; + +abstract public class HandlePurchaseTask { + + private Activity context; + + public HandlePurchaseTask(Activity context) { + this.context = context; + } + + public void handlePurchaseRequest(int resultCode, Intent data) { + //Log.d("XXX", "Handling purchase response"); + if (resultCode == Activity.RESULT_OK) { + try { + //int responseCode = data.getIntExtra("RESPONSE_CODE", 0); + PaymentsCache pc = new PaymentsCache(context); + + String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA"); + //Log.d("XXX", "Purchase data:" + purchaseData); + String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE"); + //Log.d("XXX", "Purchase signature:" + dataSignature); + //Log.d("SARLANGA", purchaseData); + + JSONObject jo = new JSONObject(purchaseData); + //String sku = jo.getString("productId"); + //alert("You have bought the " + sku + ". Excellent choice, aventurer!"); + //String orderId = jo.getString("orderId"); + //String packageName = jo.getString("packageName"); + String productId = jo.getString("productId"); + //Long purchaseTime = jo.getLong("purchaseTime"); + //Integer state = jo.getInt("purchaseState"); + String developerPayload = jo.getString("developerPayload"); + String purchaseToken = jo.getString("purchaseToken"); + + if (!pc.getConsumableValue("validation_hash", productId).equals(developerPayload)) { + error("Untrusted callback"); + return; + } + //Log.d("XXX", "Este es el product ID:" + productId); + pc.setConsumableValue("ticket_signautre", productId, dataSignature); + pc.setConsumableValue("ticket", productId, purchaseData); + pc.setConsumableFlag("block", productId, true); + pc.setConsumableValue("token", productId, purchaseToken); + + success(productId, dataSignature, purchaseData); + return; + } catch (JSONException e) { + error(e.getMessage()); + } + } else if (resultCode == Activity.RESULT_CANCELED) { + canceled(); + } + } + + abstract protected void success(String sku, String signature, String ticket); + abstract protected void error(String message); + abstract protected void canceled(); +} diff --git a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PaymentsCache.java b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PaymentsCache.java new file mode 100644 index 0000000000..d5919e3d9d --- /dev/null +++ b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PaymentsCache.java @@ -0,0 +1,71 @@ +/*************************************************************************/ +/* PaymentsCache.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +package org.godotengine.godot.plugin.payment; + +import android.content.Context; +import android.content.SharedPreferences; + +public class PaymentsCache { + + public Context context; + + public PaymentsCache(Context context) { + this.context = context; + } + + public void setConsumableFlag(String set, String sku, Boolean flag) { + SharedPreferences sharedPref = context.getSharedPreferences("consumables_" + set, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putBoolean(sku, flag); + editor.apply(); + } + + public boolean getConsumableFlag(String set, String sku) { + SharedPreferences sharedPref = context.getSharedPreferences( + "consumables_" + set, Context.MODE_PRIVATE); + return sharedPref.getBoolean(sku, false); + } + + public void setConsumableValue(String set, String sku, String value) { + SharedPreferences sharedPref = context.getSharedPreferences("consumables_" + set, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putString(sku, value); + //Log.d("XXX", "Setting asset: consumables_" + set + ":" + sku); + editor.apply(); + } + + public String getConsumableValue(String set, String sku) { + SharedPreferences sharedPref = context.getSharedPreferences( + "consumables_" + set, Context.MODE_PRIVATE); + //Log.d("XXX", "Getting asset: consumables_" + set + ":" + sku); + return sharedPref.getString(sku, null); + } +} diff --git a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PaymentsManager.java b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PaymentsManager.java new file mode 100644 index 0000000000..bded1f452f --- /dev/null +++ b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PaymentsManager.java @@ -0,0 +1,405 @@ +/*************************************************************************/ +/* PaymentsManager.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +package org.godotengine.godot.plugin.payment; + +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 com.android.vending.billing.IInAppBillingService; +import java.util.ArrayList; +import java.util.Arrays; +import org.json.JSONException; +import org.json.JSONObject; + +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 final Activity activity; + private final GodotPayment godotPayment; + IInAppBillingService mService; + + PaymentsManager(Activity activity, GodotPayment godotPayment) { + this.activity = activity; + this.godotPayment = godotPayment; + } + + public PaymentsManager initService() { + Intent intent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); + intent.setPackage("com.android.vending"); + activity.bindService( + intent, + mServiceConn, + Context.BIND_AUTO_CREATE); + return this; + } + + public void destroy() { + if (mService != null) { + activity.unbindService(mServiceConn); + } + } + + ServiceConnection mServiceConn = new ServiceConnection() { + @Override + public void onServiceDisconnected(ComponentName name) { + mService = null; + + // At this stage, godotPayment might not have been initialized yet. + if (godotPayment != null) { + godotPayment.callbackDisconnected(); + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mService = IInAppBillingService.Stub.asInterface(service); + + // At this stage, godotPayment might not have been initialized yet. + if (godotPayment != null) { + godotPayment.callbackConnected(); + } + } + }; + + public void requestPurchase(final String sku, String transactionId) { + new PurchaseTask(mService, activity) { + @Override + protected void error(String message) { + godotPayment.callbackFail(message); + } + + @Override + protected void canceled() { + godotPayment.callbackCancel(); + } + + @Override + protected void alreadyOwned() { + godotPayment.callbackAlreadyOwned(sku); + } + } + .purchase(sku, transactionId); + } + + public boolean isConnected() { + return mService != null; + } + + public void consumeUnconsumedPurchases() { + new ReleaseAllConsumablesTask(mService, activity) { + @Override + protected void success(String sku, String receipt, String signature, String token) { + godotPayment.callbackSuccessProductMassConsumed(receipt, signature, sku); + } + + @Override + protected void error(String message) { + Log.d("godot", "consumeUnconsumedPurchases :" + message); + godotPayment.callbackFailConsume(message); + } + + @Override + protected void notRequired() { + Log.d("godot", "callbackSuccessNoUnconsumedPurchases :"); + godotPayment.callbackSuccessNoUnconsumedPurchases(); + } + } + .consumeItAll(); + } + + public void requestPurchased() { + try { + PaymentsCache pc = new PaymentsCache(activity); + + String continueToken = null; + + do { + Bundle bundle = mService.getPurchases(3, activity.getPackageName(), "inapp", continueToken); + + if (bundle.getInt("RESPONSE_CODE") == 0) { + + final ArrayList myPurchases = bundle.getStringArrayList("INAPP_PURCHASE_DATA_LIST"); + final ArrayList mySignatures = bundle.getStringArrayList("INAPP_DATA_SIGNATURE_LIST"); + + if (myPurchases == null || myPurchases.size() == 0) { + godotPayment.callbackPurchased("", "", ""); + return; + } + + 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); + + godotPayment.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()); + } + } + + public void processPurchaseResponse(int resultCode, Intent data) { + new HandlePurchaseTask(activity) { + @Override + protected void success(final String sku, final String signature, final String ticket) { + godotPayment.callbackSuccess(ticket, signature, sku); + + if (auto_consume) { + new ConsumeTask(mService, activity) { + @Override + protected void success(String ticket) { + } + + @Override + protected void error(String message) { + godotPayment.callbackFail(message); + } + } + .consume(sku); + } + } + + @Override + protected void error(String message) { + godotPayment.callbackFail(message); + } + + @Override + protected void canceled() { + godotPayment.callbackCancel(); + } + } + .handlePurchaseRequest(resultCode, data); + } + + public void validatePurchase(String purchaseToken, final String sku) { + + new ValidateTask(activity, godotPayment) { + @Override + protected void success() { + + new ConsumeTask(mService, activity) { + @Override + protected void success(String ticket) { + godotPayment.callbackSuccess(ticket, null, sku); + } + + @Override + protected void error(String message) { + godotPayment.callbackFail(message); + } + } + .consume(sku); + } + + @Override + protected void error(String message) { + godotPayment.callbackFail(message); + } + + @Override + protected void canceled() { + godotPayment.callbackCancel(); + } + } + .validatePurchase(sku); + } + + public void setAutoConsume(boolean autoConsume) { + auto_consume = autoConsume; + } + + public void consume(final String sku) { + new ConsumeTask(mService, activity) { + @Override + protected void success(String ticket) { + godotPayment.callbackSuccessProductMassConsumed(ticket, "", sku); + } + + @Override + protected void error(String message) { + godotPayment.callbackFailConsume(message); + } + } + .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) { + godotPayment.errorSkuDetail(getResponseDesc(response)); + } else { + godotPayment.errorSkuDetail("No error but no detail list."); + } + return; + } + + ArrayList responseList = skuDetails.getStringArrayList("DETAILS_LIST"); + + for (String thisResponse : responseList) { + Log.d("godot", "response = " + thisResponse); + godotPayment.addSkuDetail(thisResponse); + } + } catch (RemoteException e) { + e.printStackTrace(); + godotPayment.errorSkuDetail("RemoteException error!"); + } + } + godotPayment.completeSkuDetail(); + } + })) + .start(); + } +} diff --git a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PurchaseTask.java b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PurchaseTask.java new file mode 100644 index 0000000000..eecd1d2151 --- /dev/null +++ b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PurchaseTask.java @@ -0,0 +1,118 @@ +/*************************************************************************/ +/* PurchaseTask.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +package org.godotengine.godot.plugin.payment; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Intent; +import android.content.IntentSender.SendIntentException; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import com.android.vending.billing.IInAppBillingService; + +abstract public class PurchaseTask { + + private Activity context; + + private IInAppBillingService mService; + public PurchaseTask(IInAppBillingService mService, Activity context) { + this.context = context; + this.mService = mService; + } + + private boolean isLooping = false; + + public void purchase(final String sku, final String transactionId) { + Log.d("XXX", "Starting purchase for: " + sku); + PaymentsCache pc = new PaymentsCache(context); + Boolean isBlocked = pc.getConsumableFlag("block", sku); + /* + if(isBlocked){ + Log.d("XXX", "Is awaiting payment confirmation"); + error("Awaiting payment confirmation"); + return; + } + */ + final String hash = transactionId; + + Bundle buyIntentBundle; + try { + buyIntentBundle = mService.getBuyIntent(3, context.getApplicationContext().getPackageName(), sku, "inapp", hash); + } catch (RemoteException e) { + //Log.d("XXX", "Error: " + e.getMessage()); + error(e.getMessage()); + return; + } + Object rc = buyIntentBundle.get("RESPONSE_CODE"); + int responseCode = 0; + if (rc == null) { + responseCode = PaymentsManager.BILLING_RESPONSE_RESULT_OK; + } else if (rc instanceof Integer) { + responseCode = ((Integer)rc).intValue(); + } else if (rc instanceof Long) { + responseCode = (int)((Long)rc).longValue(); + } + //Log.d("XXX", "Buy intent response code: " + responseCode); + if (responseCode == 1 || responseCode == 3 || responseCode == 4) { + canceled(); + return; + } + if (responseCode == 7) { + alreadyOwned(); + return; + } + + PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT"); + pc.setConsumableValue("validation_hash", sku, hash); + try { + if (context == null) { + //Log.d("XXX", "No context!"); + } + if (pendingIntent == null) { + //Log.d("XXX", "No pending intent"); + } + //Log.d("XXX", "Starting activity for purchase!"); + context.startIntentSenderForResult( + pendingIntent.getIntentSender(), + PaymentsManager.REQUEST_CODE_FOR_PURCHASE, + new Intent(), + Integer.valueOf(0), Integer.valueOf(0), + Integer.valueOf(0)); + } catch (SendIntentException e) { + error(e.getMessage()); + } + } + + abstract protected void error(String message); + abstract protected void canceled(); + abstract protected void alreadyOwned(); +} diff --git a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ReleaseAllConsumablesTask.java b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ReleaseAllConsumablesTask.java new file mode 100644 index 0000000000..b7bd638feb --- /dev/null +++ b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ReleaseAllConsumablesTask.java @@ -0,0 +1,141 @@ +/*************************************************************************/ +/* ReleaseAllConsumablesTask.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +package org.godotengine.godot.plugin.payment; + +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.Log; +import com.android.vending.billing.IInAppBillingService; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import org.json.JSONException; +import org.json.JSONObject; + +abstract public class ReleaseAllConsumablesTask { + + private Context context; + private IInAppBillingService mService; + + private static class ReleaseAllConsumablesAsyncTask extends AsyncTask { + + private WeakReference mTask; + private String mSku; + private String mReceipt; + private String mSignature; + private String mToken; + + ReleaseAllConsumablesAsyncTask(ReleaseAllConsumablesTask task, String sku, String receipt, String signature, String token) { + mTask = new WeakReference(task); + + mSku = sku; + mReceipt = receipt; + mSignature = signature; + mToken = token; + } + + @Override + protected String doInBackground(String... params) { + ReleaseAllConsumablesTask consume = mTask.get(); + if (consume != null) { + return consume.doInBackground(mToken); + } + return null; + } + + @Override + protected void onPostExecute(String param) { + ReleaseAllConsumablesTask consume = mTask.get(); + if (consume != null) { + consume.success(mSku, mReceipt, mSignature, mToken); + } + } + } + + public ReleaseAllConsumablesTask(IInAppBillingService mService, Context context) { + this.context = context; + this.mService = mService; + } + + public void consumeItAll() { + try { + //Log.d("godot", "consumeItall for " + context.getPackageName()); + Bundle bundle = mService.getPurchases(3, context.getPackageName(), "inapp", null); + + if (bundle.getInt("RESPONSE_CODE") == 0) { + + final ArrayList myPurchases = bundle.getStringArrayList("INAPP_PURCHASE_DATA_LIST"); + final ArrayList mySignatures = bundle.getStringArrayList("INAPP_DATA_SIGNATURE_LIST"); + + if (myPurchases == null || myPurchases.size() == 0) { + //Log.d("godot", "No purchases!"); + notRequired(); + return; + } + + //Log.d("godot", "# products to be consumed:" + myPurchases.size()); + 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); + //Log.d("godot", "A punto de consumir un item con token:" + token + "\n" + receipt); + new ReleaseAllConsumablesAsyncTask(this, sku, receipt, signature, token).execute(); + } catch (JSONException e) { + } + } + } + } catch (Exception e) { + Log.d("godot", "Error releasing products:" + e.getClass().getName() + ":" + e.getMessage()); + } + } + + private String doInBackground(String token) { + try { + //Log.d("godot", "Requesting to consume an item with token ." + token); + int response = mService.consumePurchase(3, context.getPackageName(), token); + //Log.d("godot", "consumePurchase response: " + response); + if (response == 0 || response == 8) { + return null; + } + } catch (Exception e) { + Log.d("godot", "Error " + e.getClass().getName() + ":" + e.getMessage()); + } + return null; + } + + abstract protected void success(String sku, String receipt, String signature, String token); + abstract protected void error(String message); + abstract protected void notRequired(); +} diff --git a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ValidateTask.java b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ValidateTask.java new file mode 100644 index 0000000000..179cc08ed1 --- /dev/null +++ b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ValidateTask.java @@ -0,0 +1,141 @@ +/*************************************************************************/ +/* ValidateTask.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +package org.godotengine.godot.plugin.payment; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.os.AsyncTask; +import java.lang.ref.WeakReference; +import org.godotengine.godot.utils.HttpRequester; +import org.godotengine.godot.utils.RequestParams; +import org.json.JSONException; +import org.json.JSONObject; + +abstract public class ValidateTask { + + private Activity context; + private GodotPayment godotPayments; + private ProgressDialog dialog; + private String mSku; + + private static class ValidateAsyncTask extends AsyncTask { + private WeakReference mTask; + + ValidateAsyncTask(ValidateTask task) { + mTask = new WeakReference<>(task); + } + + @Override + protected void onPreExecute() { + ValidateTask task = mTask.get(); + if (task != null) { + task.onPreExecute(); + } + } + + @Override + protected String doInBackground(String... params) { + ValidateTask task = mTask.get(); + if (task != null) { + return task.doInBackground(params); + } + return null; + } + + @Override + protected void onPostExecute(String response) { + ValidateTask task = mTask.get(); + if (task != null) { + task.onPostExecute(response); + } + } + } + + public ValidateTask(Activity context, GodotPayment godotPayments) { + this.context = context; + this.godotPayments = godotPayments; + } + + public void validatePurchase(final String sku) { + mSku = sku; + new ValidateAsyncTask(this).execute(); + } + + private void onPreExecute() { + dialog = ProgressDialog.show(context, null, "Please wait..."); + } + + private String doInBackground(String... params) { + PaymentsCache pc = new PaymentsCache(context); + String url = godotPayments.getPurchaseValidationUrlPrefix(); + RequestParams param = new RequestParams(); + param.setUrl(url); + param.put("ticket", pc.getConsumableValue("ticket", mSku)); + param.put("purchaseToken", pc.getConsumableValue("token", mSku)); + param.put("sku", mSku); + //Log.d("XXX", "Haciendo request a " + url); + //Log.d("XXX", "ticket: " + pc.getConsumableValue("ticket", sku)); + //Log.d("XXX", "purchaseToken: " + pc.getConsumableValue("token", sku)); + //Log.d("XXX", "sku: " + sku); + param.put("package", context.getApplicationContext().getPackageName()); + HttpRequester requester = new HttpRequester(); + String jsonResponse = requester.post(param); + //Log.d("XXX", "Validation response:\n"+jsonResponse); + return jsonResponse; + } + + private void onPostExecute(String response) { + if (dialog != null) { + dialog.dismiss(); + dialog = null; + } + JSONObject j; + try { + j = new JSONObject(response); + if (j.getString("status").equals("OK")) { + success(); + return; + } else if (j.getString("status") != null) { + error(j.getString("message")); + } else { + error("Connection error"); + } + } catch (JSONException e) { + error(e.getMessage()); + } catch (Exception e) { + error(e.getMessage()); + } + } + + abstract protected void success(); + abstract protected void error(String message); + abstract protected void canceled(); +} -- cgit v1.2.3