summaryrefslogtreecommitdiff
path: root/platform
diff options
context:
space:
mode:
Diffstat (limited to 'platform')
-rw-r--r--platform/android/export/export.cpp94
-rw-r--r--platform/android/java/app/AndroidManifest.xml3
-rw-r--r--platform/android/java/app/build.gradle5
-rw-r--r--platform/android/java/app/config.gradle7
-rw-r--r--platform/android/java/build.gradle2
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/Godot.java5
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java18
-rw-r--r--platform/android/java/plugins/godotpayment/build.gradle2
-rw-r--r--platform/android/java/plugins/godotpayment/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl281
-rw-r--r--platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ConsumeTask.java116
-rw-r--r--platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/GodotPayment.java309
-rw-r--r--platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/HandlePurchaseTask.java93
-rw-r--r--platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PaymentsCache.java70
-rw-r--r--platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PaymentsManager.java403
-rw-r--r--platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PurchaseTask.java118
-rw-r--r--platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ReleaseAllConsumablesTask.java140
-rw-r--r--platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ValidateTask.java143
-rw-r--r--platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/CustomSSLSocketFactory.java72
-rw-r--r--platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/GodotPaymentUtils.java66
-rw-r--r--platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/HttpRequester.java230
-rw-r--r--platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/RequestParams.java84
-rw-r--r--platform/android/plugin/godot_plugin_config.h18
-rw-r--r--platform/android/plugin/godot_plugin_jni.cpp9
-rw-r--r--platform/iphone/export/export.cpp8
-rw-r--r--platform/javascript/audio_driver_javascript.cpp4
-rw-r--r--platform/javascript/display_server_javascript.cpp4
-rw-r--r--platform/javascript/dom_keys.inc587
-rw-r--r--platform/javascript/javascript_main.cpp12
-rw-r--r--platform/linuxbsd/display_server_x11.cpp119
-rw-r--r--platform/linuxbsd/display_server_x11.h6
-rw-r--r--platform/osx/display_server_osx.h6
-rw-r--r--platform/osx/display_server_osx.mm152
-rw-r--r--platform/osx/export/export.cpp125
-rw-r--r--platform/osx/joypad_osx.cpp9
-rw-r--r--platform/uwp/export/export.cpp22
-rw-r--r--platform/uwp/os_uwp.cpp21
-rw-r--r--platform/windows/display_server_windows.cpp141
-rw-r--r--platform/windows/display_server_windows.h6
-rw-r--r--platform/windows/joypad_windows.cpp37
-rw-r--r--platform/windows/os_windows.cpp83
-rw-r--r--platform/windows/os_windows.h4
41 files changed, 1069 insertions, 2565 deletions
diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp
index dfaaf68b69..a663a847c2 100644
--- a/platform/android/export/export.cpp
+++ b/platform/android/export/export.cpp
@@ -254,6 +254,8 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
};
Vector<PluginConfig> plugins;
+ String last_plugin_names;
+ uint64_t last_custom_build_time = 0;
volatile bool plugins_changed;
Mutex plugins_lock;
Vector<Device> devices;
@@ -770,6 +772,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
bool screen_support_xlarge = p_preset->get("screen/support_xlarge");
int xr_mode_index = p_preset->get("xr_features/xr_mode");
+ bool focus_awareness = p_preset->get("xr_features/focus_awareness");
String plugins_names = get_plugins_names(get_enabled_plugins(p_preset));
@@ -926,6 +929,11 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
}
}
+ if (tname == "meta-data" && attrname == "value" && value == "oculus_focus_aware_value") {
+ // Update the focus awareness meta-data value
+ string_table.write[attr_value] = xr_mode_index == /* XRMode.OVR */ 1 && focus_awareness ? "true" : "false";
+ }
+
if (tname == "meta-data" && attrname == "value" && value == "plugins_value" && !plugins_names.empty()) {
// Update the meta-data 'android:value' attribute with the list of enabled plugins.
string_table.write[attr_value] = plugins_names;
@@ -1447,6 +1455,7 @@ public:
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/xr_mode", PROPERTY_HINT_ENUM, "Regular,Oculus Mobile VR"), 0));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/degrees_of_freedom", PROPERTY_HINT_ENUM, "None,3DOF and 6DOF,6DOF"), 0));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/hand_tracking", PROPERTY_HINT_ENUM, "None,Optional,Required"), 0));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "xr_features/focus_awareness"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "one_click_deploy/clear_previous_install"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));
@@ -1727,23 +1736,32 @@ public:
// Look for export templates (first official, and if defined custom templates).
if (!bool(p_preset->get("custom_template/use_custom_build"))) {
- bool dvalid = exists_export_template("android_debug.apk", &err);
- bool rvalid = exists_export_template("android_release.apk", &err);
+ String template_err;
+ bool dvalid = false;
+ bool rvalid = false;
if (p_preset->get("custom_template/debug") != "") {
dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));
if (!dvalid) {
- err += TTR("Custom debug template not found.") + "\n";
+ template_err += TTR("Custom debug template not found.") + "\n";
}
+ } else {
+ dvalid = exists_export_template("android_debug.apk", &template_err);
}
+
if (p_preset->get("custom_template/release") != "") {
rvalid = FileAccess::exists(p_preset->get("custom_template/release"));
if (!rvalid) {
- err += TTR("Custom release template not found.") + "\n";
+ template_err += TTR("Custom release template not found.") + "\n";
}
+ } else {
+ rvalid = exists_export_template("android_release.apk", &template_err);
}
valid = dvalid || rvalid;
+ if (!valid) {
+ err += template_err;
+ }
} else {
valid = exists_export_template("android_source.zip", &err);
}
@@ -1775,6 +1793,13 @@ public:
}
}
+ String rk = p_preset->get("keystore/release");
+
+ if (!rk.empty() && !FileAccess::exists(rk)) {
+ valid = false;
+ err += TTR("Release keystore incorrectly configured in the export preset.") + "\n";
+ }
+
if (bool(p_preset->get("custom_template/use_custom_build"))) {
String sdk_path = EditorSettings::get_singleton()->get("export/android/custom_build_sdk_path");
if (sdk_path == "") {
@@ -1821,6 +1846,40 @@ public:
err += etc_error;
}
+ // Ensure that `Use Custom Build` is enabled if a plugin is selected.
+ String enabled_plugins_names = get_plugins_names(get_enabled_plugins(p_preset));
+ bool custom_build_enabled = p_preset->get("custom_template/use_custom_build");
+ if (!enabled_plugins_names.empty() && !custom_build_enabled) {
+ valid = false;
+ err += TTR("\"Use Custom Build\" must be enabled to use the plugins.");
+ err += "\n";
+ }
+
+ // Validate the Xr features are properly populated
+ int xr_mode_index = p_preset->get("xr_features/xr_mode");
+ int degrees_of_freedom = p_preset->get("xr_features/degrees_of_freedom");
+ int hand_tracking = p_preset->get("xr_features/hand_tracking");
+ bool focus_awareness = p_preset->get("xr_features/focus_awareness");
+ if (xr_mode_index != /* XRMode.OVR*/ 1) {
+ if (degrees_of_freedom > 0) {
+ valid = false;
+ err += TTR("\"Degrees Of Freedom\" is only valid when \"Xr Mode\" is \"Oculus Mobile VR\".");
+ err += "\n";
+ }
+
+ if (hand_tracking > 0) {
+ valid = false;
+ err += TTR("\"Hand Tracking\" is only valid when \"Xr Mode\" is \"Oculus Mobile VR\".");
+ err += "\n";
+ }
+
+ if (focus_awareness) {
+ valid = false;
+ err += TTR("\"Focus Awareness\" is only valid when \"Xr Mode\" is \"Oculus Mobile VR\".");
+ err += "\n";
+ }
+ }
+
r_error = err;
return valid;
}
@@ -1831,6 +1890,29 @@ public:
return list;
}
+ inline bool is_clean_build_required(Vector<PluginConfig> enabled_plugins) {
+ String plugin_names = get_plugins_names(enabled_plugins);
+ bool first_build = last_custom_build_time == 0;
+ bool have_plugins_changed = false;
+
+ if (!first_build) {
+ have_plugins_changed = plugin_names != last_plugin_names;
+ if (!have_plugins_changed) {
+ for (int i = 0; i < enabled_plugins.size(); i++) {
+ if (enabled_plugins.get(i).last_updated > last_custom_build_time) {
+ have_plugins_changed = true;
+ break;
+ }
+ }
+ }
+ }
+
+ last_custom_build_time = OS::get_singleton()->get_unix_time();
+ last_plugin_names = plugin_names;
+
+ return have_plugins_changed || first_build;
+ }
+
virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) {
ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
@@ -1877,8 +1959,12 @@ public:
String local_plugins_binaries = get_plugins_binaries(BINARY_TYPE_LOCAL, enabled_plugins);
String remote_plugins_binaries = get_plugins_binaries(BINARY_TYPE_REMOTE, enabled_plugins);
String custom_maven_repos = get_plugins_custom_maven_repos(enabled_plugins);
+ bool clean_build_required = is_clean_build_required(enabled_plugins);
List<String> cmdline;
+ if (clean_build_required) {
+ cmdline.push_back("clean");
+ }
cmdline.push_back("build");
cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name.
cmdline.push_back("-Pplugins_local_binaries=" + local_plugins_binaries); // argument to specify the list of plugins local dependencies.
diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml
index f5b1d29f22..dbf1dc0f3c 100644
--- a/platform/android/java/app/AndroidManifest.xml
+++ b/platform/android/java/app/AndroidManifest.xml
@@ -45,6 +45,9 @@
android:resizeableActivity="false"
tools:ignore="UnusedAttribute" >
+ <!-- Focus awareness metadata populated at export time if the user enables it in the 'Xr Features' section. -->
+ <meta-data android:name="com.oculus.vr.focusaware" android:value="oculus_focus_aware_value" />
+
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle
index ea341b37b1..19202d2310 100644
--- a/platform/android/java/app/build.gradle
+++ b/platform/android/java/app/build.gradle
@@ -75,6 +75,11 @@ android {
}
defaultConfig {
+ // The default ignore pattern for the 'assets' directory includes hidden files and directories which are used by Godot projects.
+ aaptOptions {
+ ignoreAssetsPattern "!.svn:!.git:!.ds_store:!*.scc:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"
+ }
+
// Feel free to modify the application id to your own.
applicationId getExportPackageName()
minSdkVersion versions.minSdk
diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle
index 5251bc3066..acfdef531e 100644
--- a/platform/android/java/app/config.gradle
+++ b/platform/android/java/app/config.gradle
@@ -72,17 +72,12 @@ ext.getGodotPluginsRemoteBinaries = { ->
/**
* Parse the project properties for the 'plugins_local_binaries' property and return
* their binaries for inclusion in the build dependencies.
- *
- * Returns the prebuilt plugins if the 'plugins_local_binaries' property is unavailable.
*/
ext.getGodotPluginsLocalBinaries = { ->
- // Set the prebuilt plugins as default. If custom build is enabled,
- // the 'plugins_local_binaries' will be defined so we can use it instead.
- Set<String> binDeps = ["libs/plugins/GodotPayment.release.aar"]
+ Set<String> binDeps = []
// Retrieve the list of local plugins binaries.
if (project.hasProperty("plugins_local_binaries")) {
- binDeps.clear()
String pluginsList = project.property("plugins_local_binaries")
if (pluginsList != null && !pluginsList.trim().isEmpty()) {
for (String plugin : pluginsList.split(PLUGIN_VALUE_SEPARATOR_REGEX)) {
diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle
index 01a3607b20..80c6be0fae 100644
--- a/platform/android/java/build.gradle
+++ b/platform/android/java/build.gradle
@@ -193,4 +193,6 @@ task cleanGodotTemplates(type: Delete) {
delete("$binDir/android_source.zip")
delete("$binDir/godot-lib.debug.aar")
delete("$binDir/godot-lib.release.aar")
+
+ finalizedBy getTasksByName("clean", true)
}
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 f27d8620ec..fcbbc86100 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java
@@ -262,7 +262,7 @@ public abstract class Godot extends FragmentActivity implements SensorEventListe
// Include the returned non-null views in the Godot view hierarchy.
for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
- View pluginView = plugin.onMainCreateView(this);
+ View pluginView = plugin.onMainCreate(this);
if (pluginView != null) {
layout.addView(pluginView);
}
@@ -345,7 +345,8 @@ public abstract class Godot extends FragmentActivity implements SensorEventListe
return deviceInfo.reqGlEsVersion;
}
- private String[] getCommandLine() {
+ @CallSuper
+ protected String[] getCommandLine() {
InputStream is;
try {
is = getAssets().open("_cl_");
diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java
index 431bd4f5f9..ce85880fa3 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java
@@ -35,6 +35,7 @@ import org.godotengine.godot.Godot;
import android.app.Activity;
import android.content.Intent;
+import android.os.Bundle;
import android.util.Log;
import android.view.Surface;
import android.view.View;
@@ -93,6 +94,14 @@ public abstract class GodotPlugin {
}
/**
+ * Provides access to the underlying {@link Activity}.
+ */
+ @Nullable
+ protected Activity getActivity() {
+ return godot;
+ }
+
+ /**
* Register the plugin with Godot native code.
*
* This method is invoked on the render thread.
@@ -145,13 +154,14 @@ public abstract class GodotPlugin {
* Invoked once during the Godot Android initialization process after creation of the
* {@link org.godotengine.godot.GodotView} view.
* <p>
- * This method should be overridden by descendants of this class that would like to add
- * their view/layout to the Godot view hierarchy.
+ * The plugin can return a non-null {@link View} layout in order to add it to the Godot view
+ * hierarchy.
*
- * @return the view to be included; null if no views should be included.
+ * @see Activity#onCreate(Bundle)
+ * @return the plugin's view to be included; null if no views should be included.
*/
@Nullable
- public View onMainCreateView(Activity activity) {
+ public View onMainCreate(Activity activity) {
return null;
}
diff --git a/platform/android/java/plugins/godotpayment/build.gradle b/platform/android/java/plugins/godotpayment/build.gradle
index ffab86e26e..fb3aa8bba2 100644
--- a/platform/android/java/plugins/godotpayment/build.gradle
+++ b/platform/android/java/plugins/godotpayment/build.gradle
@@ -3,7 +3,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion versions.compileSdk
buildToolsVersion versions.buildTools
- useLibrary 'org.apache.http.legacy'
defaultConfig {
minSdkVersion versions.minSdk
@@ -21,6 +20,7 @@ android {
dependencies {
implementation libraries.supportCoreUtils
implementation libraries.v4Support
+ implementation 'com.android.billingclient:billing:2.2.1'
if (rootProject.findProject(":lib")) {
compileOnly project(":lib")
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
deleted file mode 100644
index 0f2bcae338..0000000000
--- a/platform/android/java/plugins/godotpayment/src/main/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<String> 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<String> - 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<String> containing the list of SKUs
- * "INAPP_PURCHASE_DATA_LIST" - ArrayList<String> containing the purchase information
- * "INAPP_DATA_SIGNATURE_LIST"- ArrayList<String> 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
deleted file mode 100644
index de009f6d16..0000000000
--- a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/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.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<String, String, String> {
- private WeakReference<ConsumeTask> 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 e2b12c94a4..bf8e485d96 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
@@ -32,214 +32,177 @@ package org.godotengine.godot.plugin.payment;
import org.godotengine.godot.Dictionary;
import org.godotengine.godot.Godot;
-import org.godotengine.godot.GodotLib;
import org.godotengine.godot.plugin.GodotPlugin;
-
-import android.content.Intent;
-import android.util.Log;
+import org.godotengine.godot.plugin.SignalInfo;
+import org.godotengine.godot.plugin.payment.utils.GodotPaymentUtils;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.collection.ArraySet;
+
+import com.android.billingclient.api.AcknowledgePurchaseParams;
+import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
+import com.android.billingclient.api.BillingClient;
+import com.android.billingclient.api.BillingClientStateListener;
+import com.android.billingclient.api.BillingFlowParams;
+import com.android.billingclient.api.BillingResult;
+import com.android.billingclient.api.ConsumeParams;
+import com.android.billingclient.api.ConsumeResponseListener;
+import com.android.billingclient.api.Purchase;
+import com.android.billingclient.api.PurchasesUpdatedListener;
+import com.android.billingclient.api.SkuDetails;
+import com.android.billingclient.api.SkuDetailsParams;
+import com.android.billingclient.api.SkuDetailsResponseListener;
-import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Set;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-public class GodotPayment extends GodotPlugin {
- private Long purchaseCallbackId = 0L;
- private String accessToken;
- private String purchaseValidationUrlPrefix;
- private String transactionId;
- private final PaymentsManager mPaymentManager;
- private final Dictionary mSkuDetails = new Dictionary();
+public class GodotPayment extends GodotPlugin implements PurchasesUpdatedListener, BillingClientStateListener {
+ private final BillingClient billingClient;
+ private final HashMap<String, SkuDetails> skuDetailsCache = new HashMap<>(); // sku → SkuDetails
public GodotPayment(Godot godot) {
super(godot);
- 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);
- }
+ billingClient = BillingClient
+ .newBuilder(getActivity())
+ .enablePendingPurchases()
+ .setListener(this)
+ .build();
}
- @Override
- public void onMainDestroy() {
- super.onMainDestroy();
- if (mPaymentManager != null) {
- mPaymentManager.destroy();
- }
+ public void startConnection() {
+ billingClient.startConnection(this);
}
- public void purchase(final String sku, final String transactionId) {
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mPaymentManager.requestPurchase(sku, transactionId);
- }
- });
- }
-
- public void consumeUnconsumedPurchases() {
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mPaymentManager.consumeUnconsumedPurchases();
- }
- });
+ public void endConnection() {
+ billingClient.endConnection();
}
- private String signature;
-
- public String getSignature() {
- return this.signature;
+ public boolean isReady() {
+ return this.billingClient.isReady();
}
- public void callbackSuccess(String ticket, String signature, String sku) {
- GodotLib.calldeferred(purchaseCallbackId, "purchase_success", new Object[] { ticket, signature, sku });
- }
+ public Dictionary queryPurchases(String type) {
+ Purchase.PurchasesResult result = billingClient.queryPurchases(type);
- public void callbackSuccessProductMassConsumed(String ticket, String signature, String sku) {
- Log.d(this.getClass().getName(), "callbackSuccessProductMassConsumed > " + ticket + "," + signature + "," + sku);
- GodotLib.calldeferred(purchaseCallbackId, "consume_success", new Object[] { ticket, signature, sku });
- }
-
- public void callbackSuccessNoUnconsumedPurchases() {
- GodotLib.calldeferred(purchaseCallbackId, "consume_not_required", new Object[] {});
- }
-
- public void callbackFailConsume(String message) {
- GodotLib.calldeferred(purchaseCallbackId, "consume_fail", new Object[] { message });
- }
-
- public void callbackFail(String message) {
- GodotLib.calldeferred(purchaseCallbackId, "purchase_fail", new Object[] { message });
- }
-
- public void callbackCancel() {
- GodotLib.calldeferred(purchaseCallbackId, "purchase_cancel", new Object[] {});
- }
-
- public void callbackAlreadyOwned(String sku) {
- GodotLib.calldeferred(purchaseCallbackId, "purchase_owned", new Object[] { sku });
- }
-
- public long getPurchaseCallbackId() {
- return purchaseCallbackId;
- }
-
- public void setPurchaseCallbackId(long purchaseCallbackId) {
- this.purchaseCallbackId = purchaseCallbackId;
- }
+ Dictionary returnValue = new Dictionary();
+ if (result.getBillingResult().getResponseCode() == BillingClient.BillingResponseCode.OK) {
+ returnValue.put("status", 0); // OK = 0
+ returnValue.put("purchases", GodotPaymentUtils.convertPurchaseListToDictionaryObjectArray(result.getPurchasesList()));
+ } else {
+ returnValue.put("status", 1); // FAILED = 1
+ returnValue.put("response_code", result.getBillingResult().getResponseCode());
+ returnValue.put("debug_message", result.getBillingResult().getDebugMessage());
+ }
- public String getPurchaseValidationUrlPrefix() {
- return this.purchaseValidationUrlPrefix;
+ return returnValue;
}
- public void setPurchaseValidationUrlPrefix(String url) {
- this.purchaseValidationUrlPrefix = url;
- }
+ public void querySkuDetails(final String[] list, String type) {
+ List<String> skuList = Arrays.asList(list);
- public String getAccessToken() {
- return accessToken;
- }
+ SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder()
+ .setSkusList(skuList)
+ .setType(type);
- public void setAccessToken(String accessToken) {
- this.accessToken = accessToken;
+ billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {
+ @Override
+ public void onSkuDetailsResponse(BillingResult billingResult,
+ List<SkuDetails> skuDetailsList) {
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
+ for (SkuDetails skuDetails : skuDetailsList) {
+ skuDetailsCache.put(skuDetails.getSku(), skuDetails);
+ }
+ emitSignal("sku_details_query_completed", (Object)GodotPaymentUtils.convertSkuDetailsListToDictionaryObjectArray(skuDetailsList));
+ } else {
+ emitSignal("sku_details_query_error", billingResult.getResponseCode(), billingResult.getDebugMessage(), list);
+ }
+ }
+ });
}
- public void setTransactionId(String transactionId) {
- this.transactionId = transactionId;
+ public void acknowledgePurchase(final String purchaseToken) {
+ AcknowledgePurchaseParams acknowledgePurchaseParams =
+ AcknowledgePurchaseParams.newBuilder()
+ .setPurchaseToken(purchaseToken)
+ .build();
+ billingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() {
+ @Override
+ public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
+ emitSignal("purchase_acknowledged", purchaseToken);
+ } else {
+ emitSignal("purchase_acknowledgement_error", billingResult.getResponseCode(), billingResult.getDebugMessage(), purchaseToken);
+ }
+ }
+ });
}
- public String getTransactionId() {
- return this.transactionId;
- }
+ public void consumePurchase(String purchaseToken) {
+ ConsumeParams consumeParams = ConsumeParams.newBuilder()
+ .setPurchaseToken(purchaseToken)
+ .build();
- // request purchased items are not consumed
- public void requestPurchased() {
- runOnUiThread(new Runnable() {
+ billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
@Override
- public void run() {
- mPaymentManager.requestPurchased();
+ public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
+ emitSignal("purchase_consumed", purchaseToken);
+ } else {
+ emitSignal("purchase_consumption_error", billingResult.getResponseCode(), billingResult.getDebugMessage(), purchaseToken);
+ }
}
});
}
- // callback for requestPurchased()
- public void callbackPurchased(String receipt, String signature, String sku) {
- GodotLib.calldeferred(purchaseCallbackId, "has_purchased", new Object[] { receipt, signature, sku });
- }
-
- public void callbackDisconnected() {
- GodotLib.calldeferred(purchaseCallbackId, "iap_disconnected", new Object[] {});
+ @Override
+ public void onBillingSetupFinished(BillingResult billingResult) {
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
+ emitSignal("connected");
+ } else {
+ emitSignal("connect_error", billingResult.getResponseCode(), billingResult.getDebugMessage());
+ }
}
- public void callbackConnected() {
- GodotLib.calldeferred(purchaseCallbackId, "iap_connected", new Object[] {});
+ @Override
+ public void onBillingServiceDisconnected() {
+ emitSignal("disconnected");
}
- // true if connected, false otherwise
- public boolean isConnected() {
- return mPaymentManager.isConnected();
- }
+ public Dictionary purchase(String sku) {
+ if (!skuDetailsCache.containsKey(sku)) {
+ emitSignal("purchase_error", null, "You must query the sku details and wait for the result before purchasing!");
+ }
- // consume item automatically after purchase. default is true.
- public void setAutoConsume(boolean autoConsume) {
- mPaymentManager.setAutoConsume(autoConsume);
- }
+ SkuDetails skuDetails = skuDetailsCache.get(sku);
+ BillingFlowParams purchaseParams = BillingFlowParams.newBuilder()
+ .setSkuDetails(skuDetails)
+ .build();
- // consume a specific item
- public void consume(String sku) {
- mPaymentManager.consume(sku);
- }
+ BillingResult result = billingClient.launchBillingFlow(getActivity(), purchaseParams);
- // query in app item detail info
- public void querySkuDetails(String[] list) {
- List<String> nKeys = Arrays.asList(list);
- List<String> cKeys = Arrays.asList(mSkuDetails.get_keys());
- ArrayList<String> fKeys = new ArrayList<String>();
- for (String key : nKeys) {
- if (!cKeys.contains(key)) {
- fKeys.add(key);
- }
- }
- if (fKeys.size() > 0) {
- mPaymentManager.querySkuDetails(fKeys.toArray(new String[0]));
+ Dictionary returnValue = new Dictionary();
+ if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
+ returnValue.put("status", 0); // OK = 0
} else {
- completeSkuDetail();
- }
- }
-
- public void addSkuDetail(String itemJson) {
- JSONObject o = null;
- try {
- o = new JSONObject(itemJson);
- Dictionary item = new Dictionary();
- item.put("type", o.optString("type"));
- item.put("product_id", o.optString("productId"));
- item.put("title", o.optString("title"));
- item.put("description", o.optString("description"));
- item.put("price", o.optString("price"));
- item.put("price_currency_code", o.optString("price_currency_code"));
- item.put("price_amount", 0.000001d * o.optLong("price_amount_micros"));
- mSkuDetails.put(item.get("product_id").toString(), item);
- } catch (JSONException e) {
- e.printStackTrace();
+ returnValue.put("status", 1); // FAILED = 1
+ returnValue.put("response_code", result.getResponseCode());
+ returnValue.put("debug_message", result.getDebugMessage());
}
- }
- public void completeSkuDetail() {
- GodotLib.calldeferred(purchaseCallbackId, "sku_details_complete", new Object[] { mSkuDetails });
+ return returnValue;
}
- public void errorSkuDetail(String errorMessage) {
- GodotLib.calldeferred(purchaseCallbackId, "sku_details_error", new Object[] { errorMessage });
+ @Override
+ public void onPurchasesUpdated(final BillingResult billingResult, @Nullable final List<Purchase> list) {
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && list != null) {
+ emitSignal("purchases_updated", (Object)GodotPaymentUtils.convertPurchaseListToDictionaryObjectArray(list));
+ } else {
+ emitSignal("purchase_error", billingResult.getResponseCode(), billingResult.getDebugMessage());
+ }
}
@NonNull
@@ -251,8 +214,26 @@ public class GodotPayment extends GodotPlugin {
@NonNull
@Override
public List<String> getPluginMethods() {
- return Arrays.asList("purchase", "setPurchaseCallbackId", "setPurchaseValidationUrlPrefix",
- "setTransactionId", "getSignature", "consumeUnconsumedPurchases", "requestPurchased",
- "setAutoConsume", "consume", "querySkuDetails", "isConnected");
+ return Arrays.asList("startConnection", "endConnection", "purchase", "querySkuDetails", "isReady", "queryPurchases", "acknowledgePurchase", "consumePurchase");
+ }
+
+ @NonNull
+ @Override
+ public Set<SignalInfo> getPluginSignals() {
+ Set<SignalInfo> signals = new ArraySet<>();
+
+ signals.add(new SignalInfo("connected"));
+ signals.add(new SignalInfo("disconnected"));
+ signals.add(new SignalInfo("connect_error", Integer.class, String.class));
+ signals.add(new SignalInfo("purchases_updated", Object[].class));
+ signals.add(new SignalInfo("purchase_error", Integer.class, String.class));
+ signals.add(new SignalInfo("sku_details_query_completed", Object[].class));
+ signals.add(new SignalInfo("sku_details_query_error", Integer.class, String.class, String[].class));
+ signals.add(new SignalInfo("purchase_acknowledged", String.class));
+ signals.add(new SignalInfo("purchase_acknowledgement_error", Integer.class, String.class, String.class));
+ signals.add(new SignalInfo("purchase_consumed", String.class));
+ signals.add(new SignalInfo("purchase_consumption_error", Integer.class, String.class, String.class));
+
+ return signals;
}
}
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
deleted file mode 100644
index 00e216e8c0..0000000000
--- a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/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.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
deleted file mode 100644
index 435f43c49d..0000000000
--- a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PaymentsCache.java
+++ /dev/null
@@ -1,70 +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.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
deleted file mode 100644
index 9b3a338866..0000000000
--- a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/PaymentsManager.java
+++ /dev/null
@@ -1,403 +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.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<String> myPurchases = bundle.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
- final ArrayList<String> 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<String> skuList = new ArrayList<String>(Arrays.asList(list));
- if (skuList.size() == 0) {
- return;
- }
- // Split the sku list in blocks of no more than 20 elements.
- ArrayList<ArrayList<String>> packs = new ArrayList<ArrayList<String>>();
- ArrayList<String> tempList;
- int n = skuList.size() / 20;
- int mod = skuList.size() % 20;
- for (int i = 0; i < n; i++) {
- tempList = new ArrayList<String>();
- for (String s : skuList.subList(i * 20, i * 20 + 20)) {
- tempList.add(s);
- }
- packs.add(tempList);
- }
- if (mod != 0) {
- tempList = new ArrayList<String>();
- for (String s : skuList.subList(n * 20, n * 20 + mod)) {
- tempList.add(s);
- }
- packs.add(tempList);
- }
- for (ArrayList<String> skuPartList : packs) {
- Bundle querySkus = new Bundle();
- querySkus.putStringArrayList("ITEM_ID_LIST", skuPartList);
- Bundle skuDetails = null;
- try {
- skuDetails = mService.getSkuDetails(3, activity.getPackageName(), "inapp", querySkus);
- if (!skuDetails.containsKey("DETAILS_LIST")) {
- int response = getResponseCodeFromBundle(skuDetails);
- if (response != BILLING_RESPONSE_RESULT_OK) {
- godotPayment.errorSkuDetail(getResponseDesc(response));
- } else {
- godotPayment.errorSkuDetail("No error but no detail list.");
- }
- return;
- }
-
- ArrayList<String> 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
deleted file mode 100644
index f894bd5132..0000000000
--- a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/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.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
deleted file mode 100644
index 1d52cf0fa5..0000000000
--- a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ReleaseAllConsumablesTask.java
+++ /dev/null
@@ -1,140 +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.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<String, String, String> {
- private WeakReference<ReleaseAllConsumablesTask> 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<ReleaseAllConsumablesTask>(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<String> myPurchases = bundle.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
- final ArrayList<String> 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
deleted file mode 100644
index a7156152ce..0000000000
--- a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/ValidateTask.java
+++ /dev/null
@@ -1,143 +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.plugin.payment;
-
-import org.godotengine.godot.plugin.payment.utils.HttpRequester;
-import org.godotengine.godot.plugin.payment.utils.RequestParams;
-
-import android.app.Activity;
-import android.app.ProgressDialog;
-import android.os.AsyncTask;
-
-import java.lang.ref.WeakReference;
-
-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<String, String, String> {
- private WeakReference<ValidateTask> 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();
-}
diff --git a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/CustomSSLSocketFactory.java b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/CustomSSLSocketFactory.java
deleted file mode 100644
index 55b87b49e5..0000000000
--- a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/CustomSSLSocketFactory.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*************************************************************************/
-/* CustomSSLSocketFactory.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.utils;
-
-import java.io.IOException;
-import java.net.Socket;
-import java.net.UnknownHostException;
-import java.security.KeyManagementException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.UnrecoverableKeyException;
-
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.TrustManagerFactory;
-
-import org.apache.http.conn.ssl.SSLSocketFactory;
-
-/**
- *
- * @author Luis Linietsky <luis.linietsky@gmail.com>
- */
-public class CustomSSLSocketFactory extends SSLSocketFactory {
- SSLContext sslContext = SSLContext.getInstance("TLS");
-
- public CustomSSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
- super(truststore);
-
- TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
- tmf.init(truststore);
-
- sslContext.init(null, tmf.getTrustManagers(), null);
- }
-
- @Override
- public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
- return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
- }
-
- @Override
- public Socket createSocket() throws IOException {
- return sslContext.getSocketFactory().createSocket();
- }
-}
diff --git a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/GodotPaymentUtils.java b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/GodotPaymentUtils.java
new file mode 100644
index 0000000000..f569c1b8bf
--- /dev/null
+++ b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/GodotPaymentUtils.java
@@ -0,0 +1,66 @@
+package org.godotengine.godot.plugin.payment.utils;
+
+import org.godotengine.godot.Dictionary;
+
+import com.android.billingclient.api.Purchase;
+import com.android.billingclient.api.SkuDetails;
+
+import java.util.List;
+
+public class GodotPaymentUtils {
+ public static Dictionary convertPurchaseToDictionary(Purchase purchase) {
+ Dictionary dictionary = new Dictionary();
+ dictionary.put("order_id", purchase.getOrderId());
+ dictionary.put("package_name", purchase.getPackageName());
+ dictionary.put("purchase_state", Integer.valueOf(purchase.getPurchaseState()));
+ dictionary.put("purchase_time", Long.valueOf(purchase.getPurchaseTime()));
+ dictionary.put("purchase_token", purchase.getPurchaseToken());
+ dictionary.put("signature", purchase.getSignature());
+ dictionary.put("sku", purchase.getSku());
+ dictionary.put("is_acknowledged", Boolean.valueOf(purchase.isAcknowledged()));
+ dictionary.put("is_auto_renewing", Boolean.valueOf(purchase.isAutoRenewing()));
+ return dictionary;
+ }
+
+ public static Dictionary convertSkuDetailsToDictionary(SkuDetails details) {
+ Dictionary dictionary = new Dictionary();
+ dictionary.put("sku", details.getSku());
+ dictionary.put("title", details.getTitle());
+ dictionary.put("description", details.getDescription());
+ dictionary.put("price", details.getPrice());
+ dictionary.put("price_currency_code", details.getPriceCurrencyCode());
+ dictionary.put("price_amount_micros", Long.valueOf(details.getPriceAmountMicros()));
+ dictionary.put("free_trial_period", details.getFreeTrialPeriod());
+ dictionary.put("icon_url", details.getIconUrl());
+ dictionary.put("introductory_price", details.getIntroductoryPrice());
+ dictionary.put("introductory_price_amount_micros", Long.valueOf(details.getIntroductoryPriceAmountMicros()));
+ dictionary.put("introductory_price_cycles", details.getIntroductoryPriceCycles());
+ dictionary.put("introductory_price_period", details.getIntroductoryPricePeriod());
+ dictionary.put("original_price", details.getOriginalPrice());
+ dictionary.put("original_price_amount_micros", Long.valueOf(details.getOriginalPriceAmountMicros()));
+ dictionary.put("subscription_period", details.getSubscriptionPeriod());
+ dictionary.put("type", details.getType());
+ dictionary.put("is_rewarded", Boolean.valueOf(details.isRewarded()));
+ return dictionary;
+ }
+
+ public static Object[] convertPurchaseListToDictionaryObjectArray(List<Purchase> purchases) {
+ Object[] purchaseDictionaries = new Object[purchases.size()];
+
+ for (int i = 0; i < purchases.size(); i++) {
+ purchaseDictionaries[i] = GodotPaymentUtils.convertPurchaseToDictionary(purchases.get(i));
+ }
+
+ return purchaseDictionaries;
+ }
+
+ public static Object[] convertSkuDetailsListToDictionaryObjectArray(List<SkuDetails> skuDetails) {
+ Object[] skuDetailsDictionaries = new Object[skuDetails.size()];
+
+ for (int i = 0; i < skuDetails.size(); i++) {
+ skuDetailsDictionaries[i] = GodotPaymentUtils.convertSkuDetailsToDictionary(skuDetails.get(i));
+ }
+
+ return skuDetailsDictionaries;
+ }
+}
diff --git a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/HttpRequester.java b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/HttpRequester.java
deleted file mode 100644
index 0afcf60f38..0000000000
--- a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/HttpRequester.java
+++ /dev/null
@@ -1,230 +0,0 @@
-/*************************************************************************/
-/* HttpRequester.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.utils;
-
-import org.godotengine.godot.utils.Crypt;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.util.Log;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.UnsupportedEncodingException;
-import java.security.KeyStore;
-import java.util.Date;
-
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpVersion;
-import org.apache.http.client.ClientProtocolException;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.entity.UrlEncodedFormEntity;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.conn.ClientConnectionManager;
-import org.apache.http.conn.scheme.PlainSocketFactory;
-import org.apache.http.conn.scheme.Scheme;
-import org.apache.http.conn.scheme.SchemeRegistry;
-import org.apache.http.conn.ssl.SSLSocketFactory;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
-import org.apache.http.params.BasicHttpParams;
-import org.apache.http.params.HttpConnectionParams;
-import org.apache.http.params.HttpParams;
-import org.apache.http.params.HttpProtocolParams;
-import org.apache.http.protocol.HTTP;
-import org.apache.http.util.EntityUtils;
-
-/**
- *
- * @author Luis Linietsky <luis.linietsky@gmail.com>
- */
-public class HttpRequester {
- private Context context;
- private static final int TTL = 600000; // 10 minutos
- private long cttl = 0;
-
- public HttpRequester() {
- //Log.d("XXX", "Creando http request sin contexto");
- }
-
- public HttpRequester(Context context) {
- this.context = context;
- //Log.d("XXX", "Creando http request con contexto");
- }
-
- public String post(RequestParams params) {
- HttpPost httppost = new HttpPost(params.getUrl());
- try {
- httppost.setEntity(new UrlEncodedFormEntity(params.toPairsList()));
- return request(httppost);
- } catch (UnsupportedEncodingException e) {
- return null;
- }
- }
-
- public String get(RequestParams params) {
- String response = getResponseFromCache(params.getUrl());
- if (response == null) {
- //Log.d("XXX", "Cache miss!");
- HttpGet httpget = new HttpGet(params.getUrl());
- long timeInit = new Date().getTime();
- response = request(httpget);
- long delay = new Date().getTime() - timeInit;
- Log.d("HttpRequest::get(url)", "Url: " + params.getUrl() + " downloaded in " + String.format("%.03f", delay / 1000.0f) + " seconds");
- if (response == null || response.length() == 0) {
- response = "";
- } else {
- saveResponseIntoCache(params.getUrl(), response);
- }
- }
- Log.d("XXX", "Req: " + params.getUrl());
- Log.d("XXX", "Resp: " + response);
- return response;
- }
-
- private String request(HttpUriRequest request) {
- //Log.d("XXX", "Haciendo request a: " + request.getURI() );
- Log.d("PPP", "Haciendo request a: " + request.getURI());
- long init = new Date().getTime();
- HttpClient httpclient = getNewHttpClient();
- HttpParams httpParameters = httpclient.getParams();
- HttpConnectionParams.setConnectionTimeout(httpParameters, 0);
- HttpConnectionParams.setSoTimeout(httpParameters, 0);
- HttpConnectionParams.setTcpNoDelay(httpParameters, true);
- try {
- HttpResponse response = httpclient.execute(request);
- Log.d("PPP", "Fin de request (" + (new Date().getTime() - init) + ") a: " + request.getURI());
- //Log.d("XXX1", "Status:" + response.getStatusLine().toString());
- if (response.getStatusLine().getStatusCode() == 200) {
- String strResponse = EntityUtils.toString(response.getEntity());
- //Log.d("XXX2", strResponse);
- return strResponse;
- } else {
- Log.d("XXX3", "Response status code:" + response.getStatusLine().getStatusCode() + "\n" + EntityUtils.toString(response.getEntity()));
- return null;
- }
-
- } catch (ClientProtocolException e) {
- Log.d("XXX3", e.getMessage());
- } catch (IOException e) {
- Log.d("XXX4", e.getMessage());
- }
- return null;
- }
-
- private HttpClient getNewHttpClient() {
- try {
- KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
- trustStore.load(null, null);
-
- SSLSocketFactory sf = new CustomSSLSocketFactory(trustStore);
- sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
-
- HttpParams params = new BasicHttpParams();
- HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
- HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);
-
- SchemeRegistry registry = new SchemeRegistry();
- registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
- registry.register(new Scheme("https", sf, 443));
-
- ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);
-
- return new DefaultHttpClient(ccm, params);
- } catch (Exception e) {
- return new DefaultHttpClient();
- }
- }
-
- private static String convertStreamToString(InputStream is) {
- BufferedReader reader = new BufferedReader(new InputStreamReader(is));
- StringBuilder sb = new StringBuilder();
- String line = null;
- try {
- while ((line = reader.readLine()) != null) {
- sb.append((line + "\n"));
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- try {
- is.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- return sb.toString();
- }
-
- public void saveResponseIntoCache(String request, String response) {
- if (context == null) {
- //Log.d("XXX", "No context, cache failed!");
- return;
- }
- SharedPreferences sharedPref = context.getSharedPreferences("http_get_cache", Context.MODE_PRIVATE);
- SharedPreferences.Editor editor = sharedPref.edit();
- editor.putString("request_" + Crypt.md5(request), response);
- editor.putLong("request_" + Crypt.md5(request) + "_ttl", new Date().getTime() + getTtl());
- editor.apply();
- }
-
- public String getResponseFromCache(String request) {
- if (context == null) {
- Log.d("XXX", "No context, cache miss");
- return null;
- }
- SharedPreferences sharedPref = context.getSharedPreferences("http_get_cache", Context.MODE_PRIVATE);
- long ttl = getResponseTtl(request);
- if (ttl == 0l || (new Date().getTime() - ttl) > 0l) {
- Log.d("XXX", "Cache invalid ttl:" + ttl + " vs now:" + new Date().getTime());
- return null;
- }
- return sharedPref.getString("request_" + Crypt.md5(request), null);
- }
-
- public long getResponseTtl(String request) {
- SharedPreferences sharedPref = context.getSharedPreferences(
- "http_get_cache", Context.MODE_PRIVATE);
- return sharedPref.getLong("request_" + Crypt.md5(request) + "_ttl", 0l);
- }
-
- public long getTtl() {
- return cttl > 0 ? cttl : TTL;
- }
-
- public void setTtl(long ttl) {
- this.cttl = (ttl * 1000) + new Date().getTime();
- }
-}
diff --git a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/RequestParams.java b/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/RequestParams.java
deleted file mode 100644
index 6b66c7e474..0000000000
--- a/platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/utils/RequestParams.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*************************************************************************/
-/* RequestParams.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.utils;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-
-import org.apache.http.NameValuePair;
-import org.apache.http.message.BasicNameValuePair;
-
-/**
- *
- * @author Luis Linietsky <luis.linietsky@gmail.com>
- */
-public class RequestParams {
- private HashMap<String, String> params;
- private String url;
-
- public RequestParams() {
- params = new HashMap<String, String>();
- }
-
- public void put(String key, String value) {
- params.put(key, value);
- }
-
- public String get(String key) {
- return params.get(key);
- }
-
- public void remove(Object key) {
- params.remove(key);
- }
-
- public boolean has(String key) {
- return params.containsKey(key);
- }
-
- public List<NameValuePair> toPairsList() {
- List<NameValuePair> fields = new ArrayList<NameValuePair>();
-
- for (String key : params.keySet()) {
- fields.add(new BasicNameValuePair(key, this.get(key)));
- }
- return fields;
- }
-
- public String getUrl() {
- return url;
- }
-
- public void setUrl(String url) {
- this.url = url;
- }
-}
diff --git a/platform/android/plugin/godot_plugin_config.h b/platform/android/plugin/godot_plugin_config.h
index 9ad7de1202..5bc0fc3a58 100644
--- a/platform/android/plugin/godot_plugin_config.h
+++ b/platform/android/plugin/godot_plugin_config.h
@@ -70,6 +70,8 @@ The `dependencies` section and fields are optional and defined as follow:
struct PluginConfig {
// Set to true when the config file is properly loaded.
bool valid_config = false;
+ // Unix timestamp of last change to this plugin.
+ uint64_t last_updated = 0;
// Required config section
String name;
@@ -87,11 +89,12 @@ struct PluginConfig {
*/
static const PluginConfig GODOT_PAYMENT = {
/*.valid_config =*/true,
+ /*.last_updated =*/0,
/*.name =*/"GodotPayment",
/*.binary_type =*/"local",
/*.binary =*/"res://android/build/libs/plugins/GodotPayment.release.aar",
/*.local_dependencies =*/{},
- /*.remote_dependencies =*/{},
+ /*.remote_dependencies =*/String("com.android.billingclient:billing:2.2.1").split("|"),
/*.custom_maven_repos =*/{}
};
@@ -150,6 +153,18 @@ static inline bool is_plugin_config_valid(PluginConfig plugin_config) {
return valid_name && valid_binary && valid_binary_type && valid_local_dependencies;
}
+static inline uint64_t get_plugin_modification_time(const PluginConfig &plugin_config, const String &config_path) {
+ uint64_t last_updated = FileAccess::get_modified_time(config_path);
+ last_updated = MAX(last_updated, FileAccess::get_modified_time(plugin_config.binary));
+
+ for (int i = 0; i < plugin_config.local_dependencies.size(); i++) {
+ String binary = plugin_config.local_dependencies.get(i);
+ last_updated = MAX(last_updated, FileAccess::get_modified_time(binary));
+ }
+
+ return last_updated;
+}
+
static inline PluginConfig load_plugin_config(Ref<ConfigFile> config_file, const String &path) {
PluginConfig plugin_config = {};
@@ -177,6 +192,7 @@ static inline PluginConfig load_plugin_config(Ref<ConfigFile> config_file, const
}
plugin_config.valid_config = is_plugin_config_valid(plugin_config);
+ plugin_config.last_updated = get_plugin_modification_time(plugin_config, path);
}
}
diff --git a/platform/android/plugin/godot_plugin_jni.cpp b/platform/android/plugin/godot_plugin_jni.cpp
index 557743fa73..d2528bebeb 100644
--- a/platform/android/plugin/godot_plugin_jni.cpp
+++ b/platform/android/plugin/godot_plugin_jni.cpp
@@ -114,12 +114,15 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeEmitS
String signal_name = jstring_to_string(j_signal_name, env);
int count = env->GetArrayLength(j_signal_params);
- const Variant *args[count];
+ ERR_FAIL_COND_MSG(count > VARIANT_ARG_MAX, "Maximum argument count exceeded!");
+
+ Variant variant_params[VARIANT_ARG_MAX];
+ const Variant *args[VARIANT_ARG_MAX];
for (int i = 0; i < count; i++) {
jobject j_param = env->GetObjectArrayElement(j_signal_params, i);
- Variant variant = _jobject_to_variant(env, j_param);
- args[i] = &variant;
+ variant_params[i] = _jobject_to_variant(env, j_param);
+ args[i] = &variant_params[i];
env->DeleteLocalRef(j_param);
};
diff --git a/platform/iphone/export/export.cpp b/platform/iphone/export/export.cpp
index 63c3cb8c23..aadc60175b 100644
--- a/platform/iphone/export/export.cpp
+++ b/platform/iphone/export/export.cpp
@@ -217,7 +217,7 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options)
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/info"), "Made with Godot Engine"));
- r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/bundle_identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/signature"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/short_version"), "1.0"));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version"), "1.0"));
@@ -293,8 +293,8 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_
strnew += lines[i].replace("$name", p_config.pkg_name) + "\n";
} else if (lines[i].find("$info") != -1) {
strnew += lines[i].replace("$info", p_preset->get("application/info")) + "\n";
- } else if (lines[i].find("$identifier") != -1) {
- strnew += lines[i].replace("$identifier", p_preset->get("application/identifier")) + "\n";
+ } else if (lines[i].find("$bundle_identifier") != -1) {
+ strnew += lines[i].replace("$bundle_identifier", p_preset->get("application/bundle_identifier")) + "\n";
} else if (lines[i].find("$short_version") != -1) {
strnew += lines[i].replace("$short_version", p_preset->get("application/short_version")) + "\n";
} else if (lines[i].find("$version") != -1) {
@@ -1346,7 +1346,7 @@ bool EditorExportPlatformIOS::can_export(const Ref<EditorExportPreset> &p_preset
valid = false;
}
- String identifier = p_preset->get("application/identifier");
+ String identifier = p_preset->get("application/bundle_identifier");
String pn_err;
if (!is_package_name_valid(identifier, &pn_err)) {
err += TTR("Invalid Identifier:") + " " + pn_err + "\n";
diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp
index b52bd4ce60..b8914414e6 100644
--- a/platform/javascript/audio_driver_javascript.cpp
+++ b/platform/javascript/audio_driver_javascript.cpp
@@ -70,14 +70,14 @@ Error AudioDriverJavaScript::init() {
/* clang-format off */
_driver_id = EM_ASM_INT({
const MIX_RATE = $0;
- const LATENCY = $1;
+ const LATENCY = $1 / 1000;
return Module.IDHandler.add({
'context': new (window.AudioContext || window.webkitAudioContext)({ sampleRate: MIX_RATE, latencyHint: LATENCY}),
'input': null,
'stream': null,
'script': null
});
- });
+ }, mix_rate, latency);
/* clang-format on */
int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode());
diff --git a/platform/javascript/display_server_javascript.cpp b/platform/javascript/display_server_javascript.cpp
index b95674efc3..0312efb377 100644
--- a/platform/javascript/display_server_javascript.cpp
+++ b/platform/javascript/display_server_javascript.cpp
@@ -143,8 +143,8 @@ static Ref<InputEventKey> setup_key_event(const EmscriptenKeyboardEvent *emscrip
ev.instance();
ev->set_echo(emscripten_event->repeat);
dom2godot_mod(emscripten_event, ev);
- ev->set_keycode(dom2godot_keycode(emscripten_event->keyCode));
- ev->set_physical_keycode(dom2godot_keycode(emscripten_event->keyCode));
+ ev->set_keycode(dom_code2godot_scancode(emscripten_event->code, emscripten_event->key, false));
+ ev->set_physical_keycode(dom_code2godot_scancode(emscripten_event->code, emscripten_event->key, true));
String unicode = String::utf8(emscripten_event->key);
// Check if empty or multi-character (e.g. `CapsLock`).
diff --git a/platform/javascript/dom_keys.inc b/platform/javascript/dom_keys.inc
index 882e943471..e3f2ce42b4 100644
--- a/platform/javascript/dom_keys.inc
+++ b/platform/javascript/dom_keys.inc
@@ -30,400 +30,203 @@
#include "core/os/keyboard.h"
-// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#Constants_for_keyCode_value
-#define DOM_VK_CANCEL 0x03
-#define DOM_VK_HELP 0x06
-#define DOM_VK_BACK_SPACE 0x08
-#define DOM_VK_TAB 0x09
-#define DOM_VK_CLEAR 0x0C
-#define DOM_VK_RETURN 0x0D
-#define DOM_VK_ENTER 0x0E // "Reserved, but not used."
-#define DOM_VK_SHIFT 0x10
-#define DOM_VK_CONTROL 0x11
-#define DOM_VK_ALT 0x12
-#define DOM_VK_PAUSE 0x13
-#define DOM_VK_CAPS_LOCK 0x14
-#define DOM_VK_KANA 0x15
-#define DOM_VK_HANGUL 0x15
-#define DOM_VK_EISU 0x16
-#define DOM_VK_JUNJA 0x17
-#define DOM_VK_FINAL 0x18
-#define DOM_VK_HANJA 0x19
-#define DOM_VK_KANJI 0x19
-#define DOM_VK_ESCAPE 0x1B
-#define DOM_VK_CONVERT 0x1C
-#define DOM_VK_NONCONVERT 0x1D
-#define DOM_VK_ACCEPT 0x1E
-#define DOM_VK_MODECHANGE 0x1F
-#define DOM_VK_SPACE 0x20
-#define DOM_VK_PAGE_UP 0x21
-#define DOM_VK_PAGE_DOWN 0x22
-#define DOM_VK_END 0x23
-#define DOM_VK_HOME 0x24
-#define DOM_VK_LEFT 0x25
-#define DOM_VK_UP 0x26
-#define DOM_VK_RIGHT 0x27
-#define DOM_VK_DOWN 0x28
-#define DOM_VK_SELECT 0x29
-#define DOM_VK_PRINT 0x2A
-#define DOM_VK_EXECUTE 0x2B
-#define DOM_VK_PRINTSCREEN 0x2C
-#define DOM_VK_INSERT 0x2D
-#define DOM_VK_DELETE 0x2E
-#define DOM_VK_0 0x30
-#define DOM_VK_1 0x31
-#define DOM_VK_2 0x32
-#define DOM_VK_3 0x33
-#define DOM_VK_4 0x34
-#define DOM_VK_5 0x35
-#define DOM_VK_6 0x36
-#define DOM_VK_7 0x37
-#define DOM_VK_8 0x38
-#define DOM_VK_9 0x39
-#define DOM_VK_COLON 0x3A
-#define DOM_VK_SEMICOLON 0x3B
-#define DOM_VK_LESS_THAN 0x3C
-#define DOM_VK_EQUALS 0x3D
-#define DOM_VK_GREATER_THAN 0x3E
-#define DOM_VK_QUESTION_MARK 0x3F
-#define DOM_VK_AT 0x40
-#define DOM_VK_A 0x41
-#define DOM_VK_B 0x42
-#define DOM_VK_C 0x43
-#define DOM_VK_D 0x44
-#define DOM_VK_E 0x45
-#define DOM_VK_F 0x46
-#define DOM_VK_G 0x47
-#define DOM_VK_H 0x48
-#define DOM_VK_I 0x49
-#define DOM_VK_J 0x4A
-#define DOM_VK_K 0x4B
-#define DOM_VK_L 0x4C
-#define DOM_VK_M 0x4D
-#define DOM_VK_N 0x4E
-#define DOM_VK_O 0x4F
-#define DOM_VK_P 0x50
-#define DOM_VK_Q 0x51
-#define DOM_VK_R 0x52
-#define DOM_VK_S 0x53
-#define DOM_VK_T 0x54
-#define DOM_VK_U 0x55
-#define DOM_VK_V 0x56
-#define DOM_VK_W 0x57
-#define DOM_VK_X 0x58
-#define DOM_VK_Y 0x59
-#define DOM_VK_Z 0x5A
-#define DOM_VK_WIN 0x5B
-#define DOM_VK_CONTEXT_MENU 0x5D
-#define DOM_VK_SLEEP 0x5F
-#define DOM_VK_NUMPAD0 0x60
-#define DOM_VK_NUMPAD1 0x61
-#define DOM_VK_NUMPAD2 0x62
-#define DOM_VK_NUMPAD3 0x63
-#define DOM_VK_NUMPAD4 0x64
-#define DOM_VK_NUMPAD5 0x65
-#define DOM_VK_NUMPAD6 0x66
-#define DOM_VK_NUMPAD7 0x67
-#define DOM_VK_NUMPAD8 0x68
-#define DOM_VK_NUMPAD9 0x69
-#define DOM_VK_MULTIPLY 0x6A
-#define DOM_VK_ADD 0x6B
-#define DOM_VK_SEPARATOR 0x6C
-#define DOM_VK_SUBTRACT 0x6D
-#define DOM_VK_DECIMAL 0x6E
-#define DOM_VK_DIVIDE 0x6F
-#define DOM_VK_F1 0x70
-#define DOM_VK_F2 0x71
-#define DOM_VK_F3 0x72
-#define DOM_VK_F4 0x73
-#define DOM_VK_F5 0x74
-#define DOM_VK_F6 0x75
-#define DOM_VK_F7 0x76
-#define DOM_VK_F8 0x77
-#define DOM_VK_F9 0x78
-#define DOM_VK_F10 0x79
-#define DOM_VK_F11 0x7A
-#define DOM_VK_F12 0x7B
-#define DOM_VK_F13 0x7C
-#define DOM_VK_F14 0x7D
-#define DOM_VK_F15 0x7E
-#define DOM_VK_F16 0x7F
-#define DOM_VK_F17 0x80
-#define DOM_VK_F18 0x81
-#define DOM_VK_F19 0x82
-#define DOM_VK_F20 0x83
-#define DOM_VK_F21 0x84
-#define DOM_VK_F22 0x85
-#define DOM_VK_F23 0x86
-#define DOM_VK_F24 0x87
-#define DOM_VK_NUM_LOCK 0x90
-#define DOM_VK_SCROLL_LOCK 0x91
-#define DOM_VK_WIN_OEM_FJ_JISHO 0x92
-#define DOM_VK_WIN_OEM_FJ_MASSHOU 0x93
-#define DOM_VK_WIN_OEM_FJ_TOUROKU 0x94
-#define DOM_VK_WIN_OEM_FJ_LOYA 0x95
-#define DOM_VK_WIN_OEM_FJ_ROYA 0x96
-#define DOM_VK_CIRCUMFLEX 0xA0
-#define DOM_VK_EXCLAMATION 0xA1
-#define DOM_VK_DOUBLE_QUOTE 0xA2
-#define DOM_VK_HASH 0xA3
-#define DOM_VK_DOLLAR 0xA4
-#define DOM_VK_PERCENT 0xA5
-#define DOM_VK_AMPERSAND 0xA6
-#define DOM_VK_UNDERSCORE 0xA7
-#define DOM_VK_OPEN_PAREN 0xA8
-#define DOM_VK_CLOSE_PAREN 0xA9
-#define DOM_VK_ASTERISK 0xAA
-#define DOM_VK_PLUS 0xAB
-#define DOM_VK_PIPE 0xAC
-#define DOM_VK_HYPHEN_MINUS 0xAD
-#define DOM_VK_OPEN_CURLY_BRACKET 0xAE
-#define DOM_VK_CLOSE_CURLY_BRACKET 0xAF
-#define DOM_VK_TILDE 0xB0
-#define DOM_VK_VOLUME_MUTE 0xB5
-#define DOM_VK_VOLUME_DOWN 0xB6
-#define DOM_VK_VOLUME_UP 0xB7
-#define DOM_VK_COMMA 0xBC
-#define DOM_VK_PERIOD 0xBE
-#define DOM_VK_SLASH 0xBF
-#define DOM_VK_BACK_QUOTE 0xC0
-#define DOM_VK_OPEN_BRACKET 0xDB
-#define DOM_VK_BACK_SLASH 0xDC
-#define DOM_VK_CLOSE_BRACKET 0xDD
-#define DOM_VK_QUOTE 0xDE
-#define DOM_VK_META 0xE0
-#define DOM_VK_ALTGR 0xE1
-#define DOM_VK_WIN_ICO_HELP 0xE3
-#define DOM_VK_WIN_ICO_00 0xE4
-#define DOM_VK_WIN_ICO_CLEAR 0xE6
-#define DOM_VK_WIN_OEM_RESET 0xE9
-#define DOM_VK_WIN_OEM_JUMP 0xEA
-#define DOM_VK_WIN_OEM_PA1 0xEB
-#define DOM_VK_WIN_OEM_PA2 0xEC
-#define DOM_VK_WIN_OEM_PA3 0xED
-#define DOM_VK_WIN_OEM_WSCTRL 0xEE
-#define DOM_VK_WIN_OEM_CUSEL 0xEF
-#define DOM_VK_WIN_OEM_ATTN 0xF0
-#define DOM_VK_WIN_OEM_FINISH 0xF1
-#define DOM_VK_WIN_OEM_COPY 0xF2
-#define DOM_VK_WIN_OEM_AUTO 0xF3
-#define DOM_VK_WIN_OEM_ENLW 0xF4
-#define DOM_VK_WIN_OEM_BACKTAB 0xF5
-#define DOM_VK_ATTN 0xF6
-#define DOM_VK_CRSEL 0xF7
-#define DOM_VK_EXSEL 0xF8
-#define DOM_VK_EREOF 0xF9
-#define DOM_VK_PLAY 0xFA
-#define DOM_VK_ZOOM 0xFB
-#define DOM_VK_PA1 0xFD
-#define DOM_VK_WIN_OEM_CLEAR 0xFE
-
-int dom2godot_keycode(int dom_keycode) {
- if (DOM_VK_0 <= dom_keycode && dom_keycode <= DOM_VK_Z) {
- // ASCII intersection
- return dom_keycode;
- }
-
- if (DOM_VK_NUMPAD0 <= dom_keycode && dom_keycode <= DOM_VK_NUMPAD9) {
- // Numpad numbers
- return KEY_KP_0 + (dom_keycode - DOM_VK_NUMPAD0);
+// See https://w3c.github.io/uievents-code/#code-value-tables
+int dom_code2godot_scancode(EM_UTF8 const p_code[32], EM_UTF8 const p_key[32], bool p_physical) {
+#define DOM2GODOT(p_str, p_godot_code) \
+ if (memcmp((const void *)p_str, (void *)p_code, strlen(p_str) + 1) == 0) { \
+ return KEY_##p_godot_code; \
}
- if (DOM_VK_F1 <= dom_keycode && dom_keycode <= DOM_VK_F16) {
- // F1-F16
- return KEY_F1 + (dom_keycode - DOM_VK_F1);
+ // Numpad section.
+ DOM2GODOT("NumLock", NUMLOCK);
+ DOM2GODOT("Numpad0", KP_0);
+ DOM2GODOT("Numpad1", KP_1);
+ DOM2GODOT("Numpad2", KP_2);
+ DOM2GODOT("Numpad3", KP_3);
+ DOM2GODOT("Numpad4", KP_4);
+ DOM2GODOT("Numpad5", KP_5);
+ DOM2GODOT("Numpad6", KP_6);
+ DOM2GODOT("Numpad7", KP_7);
+ DOM2GODOT("Numpad8", KP_8);
+ DOM2GODOT("Numpad9", KP_9);
+ DOM2GODOT("NumpadAdd", KP_ADD);
+ DOM2GODOT("NumpadBackspace", BACKSPACE);
+ DOM2GODOT("NumpadClear", CLEAR);
+ DOM2GODOT("NumpadClearEntry", CLEAR);
+ //DOM2GODOT("NumpadComma", UNKNOWN);
+ DOM2GODOT("NumpadDecimal", KP_PERIOD);
+ DOM2GODOT("NumpadDivide", KP_DIVIDE);
+ DOM2GODOT("NumpadEnter", KP_ENTER);
+ DOM2GODOT("NumpadEqual", EQUAL);
+ //DOM2GODOT("NumpadHash", UNKNOWN);
+ //DOM2GODOT("NumpadMemoryAdd", UNKNOWN);
+ //DOM2GODOT("NumpadMemoryClear", UNKNOWN);
+ //DOM2GODOT("NumpadMemoryRecall", UNKNOWN);
+ //DOM2GODOT("NumpadMemoryStore", UNKNOWN);
+ //DOM2GODOT("NumpadMemorySubtract", UNKNOWN);
+ DOM2GODOT("NumpadMultiply", KP_MULTIPLY);
+ DOM2GODOT("NumpadParenLeft", PARENLEFT);
+ DOM2GODOT("NumpadParenRight", PARENRIGHT);
+ DOM2GODOT("NumpadStar", KP_MULTIPLY); // or ASTERISK ?
+ DOM2GODOT("NumpadSubtract", KP_SUBTRACT);
+
+ // Printable ASCII.
+ if (!p_physical) {
+ uint8_t b0 = (uint8_t)p_key[0];
+ uint8_t b1 = (uint8_t)p_key[1];
+ uint8_t b2 = (uint8_t)p_key[2];
+ if (b1 == 0 && b0 > 0x1F && b0 < 0x7F) { // ASCII.
+ if (b0 > 0x60 && b0 < 0x7B) { // Lowercase ASCII.
+ b0 -= 32;
+ }
+ return b0;
+ }
+
+#define _U_2BYTES_MASK 0xE0
+#define _U_2BYTES 0xC0
+ // Latin-1 codes.
+ if (b2 == 0 && (b0 & _U_2BYTES_MASK) == _U_2BYTES) { // 2-bytes utf8, only known latin.
+ uint32_t key = ((b0 & ~_U_2BYTES_MASK) << 6) | (b1 & 0x3F);
+ if (key >= 0xA0 && key <= 0xDF) {
+ return key;
+ }
+ if (key >= 0xE0 && key <= 0xFF) { // Lowercase known latin.
+ key -= 0x20;
+ return key;
+ }
+ }
+#undef _U_2BYTES_MASK
+#undef _U_2BYTES
}
- switch (dom_keycode) {
- //case DOM_VK_CANCEL: return KEY_UNKNOWN;
- case DOM_VK_HELP:
- return KEY_HELP;
- case DOM_VK_BACK_SPACE:
- return KEY_BACKSPACE;
- case DOM_VK_TAB:
- return KEY_TAB;
-
- case DOM_VK_CLEAR:
- case DOM_VK_WIN_OEM_CLEAR: // OEM duplicate
- return KEY_CLEAR;
-
- case DOM_VK_RETURN:
- case DOM_VK_ENTER: // unused according to MDN
- return KEY_ENTER;
-
- case DOM_VK_SHIFT:
- return KEY_SHIFT;
- case DOM_VK_CONTROL:
- return KEY_CONTROL;
-
- case DOM_VK_ALT:
- case DOM_VK_ALTGR:
- return KEY_ALT;
-
- case DOM_VK_PAUSE:
- return KEY_PAUSE;
- case DOM_VK_CAPS_LOCK:
- return KEY_CAPSLOCK;
-
- /*
- case DOM_VK_KANA: return KEY_UNKNOWN;
- case DOM_VK_HANGUL: return KEY_UNKNOWN;
- case DOM_VK_EISU: return KEY_UNKNOWN;
- case DOM_VK_JUNJA: return KEY_UNKNOWN;
- case DOM_VK_FINAL: return KEY_UNKNOWN;
- case DOM_VK_HANJA: return KEY_UNKNOWN;
- case DOM_VK_KANJI: return KEY_UNKNOWN;
- */
-
- case DOM_VK_ESCAPE:
- return KEY_ESCAPE;
- /*
- case DOM_VK_CONVERT: return KEY_UNKNOWN;
- case DOM_VK_NONCONVERT: return KEY_UNKNOWN;
- case DOM_VK_ACCEPT: return KEY_UNKNOWN;
- case DOM_VK_MODECHANGE: return KEY_UNKNOWN;
- */
-
- case DOM_VK_SPACE:
- return KEY_SPACE;
- case DOM_VK_PAGE_UP:
- return KEY_PAGEUP;
- case DOM_VK_PAGE_DOWN:
- return KEY_PAGEDOWN;
- case DOM_VK_END:
- return KEY_END;
- case DOM_VK_HOME:
- return KEY_HOME;
- case DOM_VK_LEFT:
- return KEY_LEFT;
- case DOM_VK_UP:
- return KEY_UP;
- case DOM_VK_RIGHT:
- return KEY_RIGHT;
- case DOM_VK_DOWN:
- return KEY_DOWN;
-
- //case DOM_VK_SELECT: return KEY_UNKNOWN;
-
- case DOM_VK_PRINTSCREEN:
- case DOM_VK_PRINT:
- return KEY_PRINT;
-
- //case DOM_VK_EXECUTE: return KEY_UNKNOWN;
- case DOM_VK_INSERT:
- return KEY_INSERT;
- case DOM_VK_DELETE:
- return KEY_DELETE;
-
- case DOM_VK_META:
- case DOM_VK_WIN:
- return KEY_META;
-
- case DOM_VK_CONTEXT_MENU:
- return KEY_MENU;
- case DOM_VK_SLEEP:
- return KEY_STANDBY;
-
- // Numpad keys
- case DOM_VK_MULTIPLY:
- return KEY_KP_MULTIPLY;
- case DOM_VK_ADD:
- return KEY_KP_ADD;
- case DOM_VK_SEPARATOR:
- return KEY_KP_PERIOD; // Good enough?
- case DOM_VK_SUBTRACT:
- return KEY_KP_SUBTRACT;
- case DOM_VK_DECIMAL:
- return KEY_KP_PERIOD;
- case DOM_VK_DIVIDE:
- return KEY_KP_DIVIDE;
-
- /*
- case DOM_VK_F17: return KEY_UNKNOWN;
- case DOM_VK_F18: return KEY_UNKNOWN;
- case DOM_VK_F19: return KEY_UNKNOWN;
- case DOM_VK_F20: return KEY_UNKNOWN;
- case DOM_VK_F21: return KEY_UNKNOWN;
- case DOM_VK_F22: return KEY_UNKNOWN;
- case DOM_VK_F23: return KEY_UNKNOWN;
- case DOM_VK_F24: return KEY_UNKNOWN;
- */
-
- case DOM_VK_NUM_LOCK:
- return KEY_NUMLOCK;
- case DOM_VK_SCROLL_LOCK:
- return KEY_SCROLLLOCK;
-
- /*
- case DOM_VK_WIN_OEM_FJ_JISHO: return KEY_UNKNOWN;
- case DOM_VK_WIN_OEM_FJ_MASSHOU: return KEY_UNKNOWN;
- case DOM_VK_WIN_OEM_FJ_TOUROKU: return KEY_UNKNOWN;
- case DOM_VK_WIN_OEM_FJ_LOYA: return KEY_UNKNOWN;
- case DOM_VK_WIN_OEM_FJ_ROYA: return KEY_UNKNOWN;
- */
-
- case DOM_VK_CIRCUMFLEX:
- return KEY_ASCIICIRCUM;
- case DOM_VK_EXCLAMATION:
- return KEY_EXCLAM;
- case DOM_VK_DOUBLE_QUOTE:
- return KEY_QUOTEDBL;
- case DOM_VK_HASH:
- return KEY_NUMBERSIGN;
- case DOM_VK_DOLLAR:
- return KEY_DOLLAR;
- case DOM_VK_PERCENT:
- return KEY_PERCENT;
- case DOM_VK_AMPERSAND:
- return KEY_AMPERSAND;
- case DOM_VK_UNDERSCORE:
- return KEY_UNDERSCORE;
- case DOM_VK_OPEN_PAREN:
- return KEY_PARENLEFT;
- case DOM_VK_CLOSE_PAREN:
- return KEY_PARENRIGHT;
- case DOM_VK_ASTERISK:
- return KEY_ASTERISK;
- case DOM_VK_PLUS:
- return KEY_PLUS;
- case DOM_VK_PIPE:
- return KEY_BAR;
- case DOM_VK_HYPHEN_MINUS:
- return KEY_MINUS;
- case DOM_VK_OPEN_CURLY_BRACKET:
- return KEY_BRACELEFT;
- case DOM_VK_CLOSE_CURLY_BRACKET:
- return KEY_BRACERIGHT;
- case DOM_VK_TILDE:
- return KEY_ASCIITILDE;
-
- case DOM_VK_VOLUME_MUTE:
- return KEY_VOLUMEMUTE;
- case DOM_VK_VOLUME_DOWN:
- return KEY_VOLUMEDOWN;
- case DOM_VK_VOLUME_UP:
- return KEY_VOLUMEUP;
-
- case DOM_VK_COMMA:
- return KEY_COMMA;
- case DOM_VK_PERIOD:
- return KEY_PERIOD;
- case DOM_VK_SLASH:
- return KEY_SLASH;
- case DOM_VK_BACK_QUOTE:
- return KEY_QUOTELEFT;
- case DOM_VK_OPEN_BRACKET:
- return KEY_BRACKETLEFT;
- case DOM_VK_BACK_SLASH:
- return KEY_BACKSLASH;
- case DOM_VK_CLOSE_BRACKET:
- return KEY_BRACKETRIGHT;
- case DOM_VK_QUOTE:
- return KEY_APOSTROPHE;
-
- // The rest is OEM/unusual.
-
- default:
- return KEY_UNKNOWN;
- };
+ // Alphanumeric section.
+ DOM2GODOT("Backquote", QUOTELEFT);
+ DOM2GODOT("Backslash", BACKSLASH);
+ DOM2GODOT("BracketLeft", BRACKETLEFT);
+ DOM2GODOT("BracketRight", BRACKETRIGHT);
+ DOM2GODOT("Comma", COMMA);
+ DOM2GODOT("Digit0", 0);
+ DOM2GODOT("Digit1", 1);
+ DOM2GODOT("Digit2", 2);
+ DOM2GODOT("Digit3", 3);
+ DOM2GODOT("Digit4", 4);
+ DOM2GODOT("Digit5", 5);
+ DOM2GODOT("Digit6", 6);
+ DOM2GODOT("Digit7", 7);
+ DOM2GODOT("Digit8", 8);
+ DOM2GODOT("Digit9", 9);
+ DOM2GODOT("Equal", EQUAL);
+ DOM2GODOT("IntlBackslash", BACKSLASH);
+ //DOM2GODOT("IntlRo", UNKNOWN);
+ DOM2GODOT("IntlYen", YEN);
+
+ DOM2GODOT("KeyA", A);
+ DOM2GODOT("KeyB", B);
+ DOM2GODOT("KeyC", C);
+ DOM2GODOT("KeyD", D);
+ DOM2GODOT("KeyE", E);
+ DOM2GODOT("KeyF", F);
+ DOM2GODOT("KeyG", G);
+ DOM2GODOT("KeyH", H);
+ DOM2GODOT("KeyI", I);
+ DOM2GODOT("KeyJ", J);
+ DOM2GODOT("KeyK", K);
+ DOM2GODOT("KeyL", L);
+ DOM2GODOT("KeyM", M);
+ DOM2GODOT("KeyN", N);
+ DOM2GODOT("KeyO", O);
+ DOM2GODOT("KeyP", P);
+ DOM2GODOT("KeyQ", Q);
+ DOM2GODOT("KeyR", R);
+ DOM2GODOT("KeyS", S);
+ DOM2GODOT("KeyT", T);
+ DOM2GODOT("KeyU", U);
+ DOM2GODOT("KeyV", V);
+ DOM2GODOT("KeyW", W);
+ DOM2GODOT("KeyX", X);
+ DOM2GODOT("KeyY", Y);
+ DOM2GODOT("KeyZ", Z);
+
+ DOM2GODOT("Minus", MINUS);
+ DOM2GODOT("Period", PERIOD);
+ DOM2GODOT("Quote", APOSTROPHE);
+ DOM2GODOT("Semicolon", SEMICOLON);
+ DOM2GODOT("Slash", SLASH);
+
+ // Functional keys in the Alphanumeric section.
+ DOM2GODOT("AltLeft", ALT);
+ DOM2GODOT("AltRight", ALT);
+ DOM2GODOT("Backspace", BACKSPACE);
+ DOM2GODOT("CapsLock", CAPSLOCK);
+ DOM2GODOT("ContextMenu", MENU);
+ DOM2GODOT("ControlLeft", CONTROL);
+ DOM2GODOT("ControlRight", CONTROL);
+ DOM2GODOT("Enter", ENTER);
+ DOM2GODOT("MetaLeft", SUPER_L);
+ DOM2GODOT("MetaRight", SUPER_R);
+ DOM2GODOT("ShiftLeft", SHIFT);
+ DOM2GODOT("ShiftRight", SHIFT);
+ DOM2GODOT("Space", SPACE);
+ DOM2GODOT("Tab", TAB);
+
+ // ControlPad section.
+ DOM2GODOT("Delete", DELETE);
+ DOM2GODOT("End", END);
+ DOM2GODOT("Help", HELP);
+ DOM2GODOT("Home", HOME);
+ DOM2GODOT("Insert", INSERT);
+ DOM2GODOT("PageDown", PAGEDOWN);
+ DOM2GODOT("PageUp", PAGEUP);
+
+ // ArrowPad section.
+ DOM2GODOT("ArrowDown", DOWN);
+ DOM2GODOT("ArrowLeft", LEFT);
+ DOM2GODOT("ArrowRight", RIGHT);
+ DOM2GODOT("ArrowUp", UP);
+
+ // Function section.
+ DOM2GODOT("Escape", ESCAPE);
+ DOM2GODOT("F1", F1);
+ DOM2GODOT("F2", F2);
+ DOM2GODOT("F3", F3);
+ DOM2GODOT("F4", F4);
+ DOM2GODOT("F5", F5);
+ DOM2GODOT("F6", F6);
+ DOM2GODOT("F7", F7);
+ DOM2GODOT("F8", F8);
+ DOM2GODOT("F9", F9);
+ DOM2GODOT("F10", F10);
+ DOM2GODOT("F11", F11);
+ DOM2GODOT("F12", F12);
+ //DOM2GODOT("Fn", UNKNOWN); // never actually fired, but included in the standard draft.
+ //DOM2GODOT("FnLock", UNKNOWN);
+ DOM2GODOT("PrintScreen", PRINT);
+ DOM2GODOT("ScrollLock", SCROLLLOCK);
+ DOM2GODOT("Pause", PAUSE);
+
+ // Media keys section.
+ DOM2GODOT("BrowserBack", BACK);
+ DOM2GODOT("BrowserFavorites", FAVORITES);
+ DOM2GODOT("BrowserForward", FORWARD);
+ DOM2GODOT("BrowserHome", OPENURL);
+ DOM2GODOT("BrowserRefresh", REFRESH);
+ DOM2GODOT("BrowserSearch", SEARCH);
+ DOM2GODOT("BrowserStop", STOP);
+ //DOM2GODOT("Eject", UNKNOWN);
+ DOM2GODOT("LaunchApp1", LAUNCH0);
+ DOM2GODOT("LaunchApp2", LAUNCH1);
+ DOM2GODOT("LaunchMail", LAUNCHMAIL);
+ DOM2GODOT("MediaPlayPause", MEDIAPLAY);
+ DOM2GODOT("MediaSelect", LAUNCHMEDIA);
+ DOM2GODOT("MediaStop", MEDIASTOP);
+ DOM2GODOT("MediaTrackNext", MEDIANEXT);
+ DOM2GODOT("MediaTrackPrevious", MEDIAPREVIOUS);
+ //DOM2GODOT("Power", UNKNOWN);
+ //DOM2GODOT("Sleep", UNKNOWN);
+ DOM2GODOT("AudioVolumeDown", VOLUMEDOWN);
+ DOM2GODOT("AudioVolumeMute", VOLUMEMUTE);
+ DOM2GODOT("AudioVolumeUp", VOLUMEUP);
+ //DOM2GODOT("WakeUp", UNKNOWN);
+ return KEY_UNKNOWN;
+#undef DOM2GODOT
}
diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp
index 740a72fafa..fd61c46e63 100644
--- a/platform/javascript/javascript_main.cpp
+++ b/platform/javascript/javascript_main.cpp
@@ -80,6 +80,9 @@ extern "C" EMSCRIPTEN_KEEPALIVE void main_after_fs_sync(char *p_idbfs_err) {
Main::start();
os->get_main_loop()->init();
emscripten_resume_main_loop();
+ // Immediately run the first iteration.
+ // We are inside an animation frame, we want to immediately draw on the newly setup canvas.
+ main_loop_callback();
}
int main(int argc, char *argv[]) {
@@ -91,14 +94,15 @@ int main(int argc, char *argv[]) {
// Sync from persistent state into memory and then
// run the 'main_after_fs_sync' function.
/* clang-format off */
- EM_ASM(
+ EM_ASM({
FS.mkdir('/userfs');
FS.mount(IDBFS, {}, '/userfs');
FS.syncfs(true, function(err) {
- ccall('main_after_fs_sync', null, ['string'], [err ? err.message : ""])
+ requestAnimationFrame(function() {
+ ccall('main_after_fs_sync', null, ['string'], [err ? err.message : ""]);
+ });
});
-
- );
+ });
/* clang-format on */
return 0;
diff --git a/platform/linuxbsd/display_server_x11.cpp b/platform/linuxbsd/display_server_x11.cpp
index ef5ac66b34..a0954600a2 100644
--- a/platform/linuxbsd/display_server_x11.cpp
+++ b/platform/linuxbsd/display_server_x11.cpp
@@ -1846,37 +1846,106 @@ void DisplayServerX11::cursor_set_custom_image(const RES &p_cursor, CursorShape
}
}
-DisplayServerX11::LatinKeyboardVariant DisplayServerX11::get_latin_keyboard_variant() const {
- _THREAD_SAFE_METHOD_
-
- XkbDescRec *xkbdesc = XkbAllocKeyboard();
- ERR_FAIL_COND_V(!xkbdesc, LATIN_KEYBOARD_QWERTY);
+int DisplayServerX11::keyboard_get_layout_count() const {
+ int _group_count = 0;
+ XkbDescRec *kbd = XkbAllocKeyboard();
+ if (kbd) {
+ kbd->dpy = x11_display;
+ XkbGetControls(x11_display, XkbAllControlsMask, kbd);
+ XkbGetNames(x11_display, XkbSymbolsNameMask, kbd);
+
+ const Atom *groups = kbd->names->groups;
+ if (kbd->ctrls != NULL) {
+ _group_count = kbd->ctrls->num_groups;
+ } else {
+ while (_group_count < XkbNumKbdGroups && groups[_group_count] != None) {
+ _group_count++;
+ }
+ }
+ XkbFreeKeyboard(kbd, 0, true);
+ }
+ return _group_count;
+}
- XkbGetNames(x11_display, XkbSymbolsNameMask, xkbdesc);
- ERR_FAIL_COND_V(!xkbdesc->names, LATIN_KEYBOARD_QWERTY);
- ERR_FAIL_COND_V(!xkbdesc->names->symbols, LATIN_KEYBOARD_QWERTY);
+int DisplayServerX11::keyboard_get_current_layout() const {
+ XkbStateRec state;
+ XkbGetState(x11_display, XkbUseCoreKbd, &state);
+ return state.group;
+}
- char *layout = XGetAtomName(x11_display, xkbdesc->names->symbols);
- ERR_FAIL_COND_V(!layout, LATIN_KEYBOARD_QWERTY);
+void DisplayServerX11::keyboard_set_current_layout(int p_index) {
+ ERR_FAIL_INDEX(p_index, keyboard_get_layout_count());
+ XkbLockGroup(x11_display, XkbUseCoreKbd, p_index);
+}
- Vector<String> info = String(layout).split("+");
- ERR_FAIL_INDEX_V(1, info.size(), LATIN_KEYBOARD_QWERTY);
+String DisplayServerX11::keyboard_get_layout_language(int p_index) const {
+ String ret;
+ XkbDescRec *kbd = XkbAllocKeyboard();
+ if (kbd) {
+ kbd->dpy = x11_display;
+ XkbGetControls(x11_display, XkbAllControlsMask, kbd);
+ XkbGetNames(x11_display, XkbSymbolsNameMask, kbd);
+ XkbGetNames(x11_display, XkbGroupNamesMask, kbd);
+
+ int _group_count = 0;
+ const Atom *groups = kbd->names->groups;
+ if (kbd->ctrls != NULL) {
+ _group_count = kbd->ctrls->num_groups;
+ } else {
+ while (_group_count < XkbNumKbdGroups && groups[_group_count] != None) {
+ _group_count++;
+ }
+ }
- if (info[1].find("colemak") != -1) {
- return LATIN_KEYBOARD_COLEMAK;
- } else if (info[1].find("qwertz") != -1) {
- return LATIN_KEYBOARD_QWERTZ;
- } else if (info[1].find("azerty") != -1) {
- return LATIN_KEYBOARD_AZERTY;
- } else if (info[1].find("qzerty") != -1) {
- return LATIN_KEYBOARD_QZERTY;
- } else if (info[1].find("dvorak") != -1) {
- return LATIN_KEYBOARD_DVORAK;
- } else if (info[1].find("neo") != -1) {
- return LATIN_KEYBOARD_NEO;
+ Atom names = kbd->names->symbols;
+ if (names != None) {
+ char *name = XGetAtomName(x11_display, names);
+ Vector<String> info = String(name).split("+");
+ if (p_index >= 0 && p_index < _group_count) {
+ if (p_index + 1 < info.size()) {
+ ret = info[p_index + 1]; // Skip "pc" at the start and "inet"/"group" at the end of symbols.
+ } else {
+ ret = "en"; // No symbol for layout fallback to "en".
+ }
+ } else {
+ ERR_PRINT("Index " + itos(p_index) + "is out of bounds (" + itos(_group_count) + ").");
+ }
+ XFree(name);
+ }
+ XkbFreeKeyboard(kbd, 0, true);
}
+ return ret.substr(0, 2);
+}
+
+String DisplayServerX11::keyboard_get_layout_name(int p_index) const {
+ String ret;
+ XkbDescRec *kbd = XkbAllocKeyboard();
+ if (kbd) {
+ kbd->dpy = x11_display;
+ XkbGetControls(x11_display, XkbAllControlsMask, kbd);
+ XkbGetNames(x11_display, XkbSymbolsNameMask, kbd);
+ XkbGetNames(x11_display, XkbGroupNamesMask, kbd);
+
+ int _group_count = 0;
+ const Atom *groups = kbd->names->groups;
+ if (kbd->ctrls != NULL) {
+ _group_count = kbd->ctrls->num_groups;
+ } else {
+ while (_group_count < XkbNumKbdGroups && groups[_group_count] != None) {
+ _group_count++;
+ }
+ }
- return LATIN_KEYBOARD_QWERTY;
+ if (p_index >= 0 && p_index < _group_count) {
+ char *full_name = XGetAtomName(x11_display, groups[p_index]);
+ ret.parse_utf8(full_name);
+ XFree(full_name);
+ } else {
+ ERR_PRINT("Index " + itos(p_index) + "is out of bounds (" + itos(_group_count) + ").");
+ }
+ XkbFreeKeyboard(kbd, 0, true);
+ }
+ return ret;
}
DisplayServerX11::Property DisplayServerX11::_read_property(Display *p_display, Window p_window, Atom p_property) {
diff --git a/platform/linuxbsd/display_server_x11.h b/platform/linuxbsd/display_server_x11.h
index b5ea71f72a..f01b9a2323 100644
--- a/platform/linuxbsd/display_server_x11.h
+++ b/platform/linuxbsd/display_server_x11.h
@@ -327,7 +327,11 @@ public:
virtual CursorShape cursor_get_shape() const;
virtual void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot);
- virtual LatinKeyboardVariant get_latin_keyboard_variant() const;
+ virtual int keyboard_get_layout_count() const;
+ virtual int keyboard_get_current_layout() const;
+ virtual void keyboard_set_current_layout(int p_index);
+ virtual String keyboard_get_layout_language(int p_index) const;
+ virtual String keyboard_get_layout_name(int p_index) const;
virtual void process_events();
diff --git a/platform/osx/display_server_osx.h b/platform/osx/display_server_osx.h
index 8133dfe2c4..fddb1d0ca6 100644
--- a/platform/osx/display_server_osx.h
+++ b/platform/osx/display_server_osx.h
@@ -281,7 +281,11 @@ public:
virtual bool get_swap_ok_cancel();
- virtual LatinKeyboardVariant get_latin_keyboard_variant() const;
+ virtual int keyboard_get_layout_count() const;
+ virtual int keyboard_get_current_layout() const;
+ virtual void keyboard_set_current_layout(int p_index);
+ virtual String keyboard_get_layout_language(int p_index) const;
+ virtual String keyboard_get_layout_name(int p_index) const;
virtual void process_events();
virtual void force_process_and_drop_events();
diff --git a/platform/osx/display_server_osx.mm b/platform/osx/display_server_osx.mm
index 9a1191490c..4a94e09c1c 100644
--- a/platform/osx/display_server_osx.mm
+++ b/platform/osx/display_server_osx.mm
@@ -2987,85 +2987,129 @@ void DisplayServerOSX::cursor_set_custom_image(const RES &p_cursor, CursorShape
}
}
+struct LayoutInfo {
+ String name;
+ String code;
+};
+
+static Vector<LayoutInfo> kbd_layouts;
+static int current_layout = 0;
static bool keyboard_layout_dirty = true;
static void keyboard_layout_changed(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef user_info) {
+ kbd_layouts.clear();
+ current_layout = 0;
keyboard_layout_dirty = true;
}
-// Returns string representation of keys, if they are printable.
-static NSString *createStringForKeys(const CGKeyCode *keyCode, int length) {
- TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();
- if (!currentKeyboard)
- return nil;
+void _update_keyboard_layouts() {
+ @autoreleasepool {
+ TISInputSourceRef cur_source = TISCopyCurrentKeyboardInputSource();
+ NSString *cur_name = (NSString *)TISGetInputSourceProperty(cur_source, kTISPropertyLocalizedName);
+ CFRelease(cur_source);
- CFDataRef layoutData = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData);
- if (!layoutData)
- return nil;
+ // Enum IME layouts
+ NSDictionary *filter_ime = @{ (NSString *)kTISPropertyInputSourceType : (NSString *)kTISTypeKeyboardInputMode };
+ NSArray *list_ime = (NSArray *)TISCreateInputSourceList((CFDictionaryRef)filter_ime, false);
+ for (NSUInteger i = 0; i < [list_ime count]; i++) {
+ LayoutInfo ly;
+ NSString *name = (NSString *)TISGetInputSourceProperty((TISInputSourceRef)[list_ime objectAtIndex:i], kTISPropertyLocalizedName);
+ ly.name.parse_utf8([name UTF8String]);
- const UCKeyboardLayout *keyboardLayout = (const UCKeyboardLayout *)CFDataGetBytePtr(layoutData);
+ NSArray *langs = (NSArray *)TISGetInputSourceProperty((TISInputSourceRef)[list_ime objectAtIndex:i], kTISPropertyInputSourceLanguages);
+ ly.code.parse_utf8([(NSString *)[langs objectAtIndex:0] UTF8String]);
+ kbd_layouts.push_back(ly);
- OSStatus err;
- CFMutableStringRef output = CFStringCreateMutable(NULL, 0);
+ if ([name isEqualToString:cur_name]) {
+ current_layout = kbd_layouts.size() - 1;
+ }
+ }
+ [list_ime release];
- for (int i = 0; i < length; ++i) {
- UInt32 keysDown = 0;
- UniChar chars[4];
- UniCharCount realLength;
+ // Enum plain keyboard layouts
+ NSDictionary *filter_kbd = @{ (NSString *)kTISPropertyInputSourceType : (NSString *)kTISTypeKeyboardLayout };
+ NSArray *list_kbd = (NSArray *)TISCreateInputSourceList((CFDictionaryRef)filter_kbd, false);
+ for (NSUInteger i = 0; i < [list_kbd count]; i++) {
+ LayoutInfo ly;
+ NSString *name = (NSString *)TISGetInputSourceProperty((TISInputSourceRef)[list_kbd objectAtIndex:i], kTISPropertyLocalizedName);
+ ly.name.parse_utf8([name UTF8String]);
- err = UCKeyTranslate(keyboardLayout,
- keyCode[i],
- kUCKeyActionDisplay,
- 0,
- LMGetKbdType(),
- kUCKeyTranslateNoDeadKeysBit,
- &keysDown,
- sizeof(chars) / sizeof(chars[0]),
- &realLength,
- chars);
+ NSArray *langs = (NSArray *)TISGetInputSourceProperty((TISInputSourceRef)[list_kbd objectAtIndex:i], kTISPropertyInputSourceLanguages);
+ ly.code.parse_utf8([(NSString *)[langs objectAtIndex:0] UTF8String]);
+ kbd_layouts.push_back(ly);
- if (err != noErr) {
- CFRelease(output);
- return nil;
+ if ([name isEqualToString:cur_name]) {
+ current_layout = kbd_layouts.size() - 1;
+ }
}
-
- CFStringAppendCharacters(output, chars, 1);
+ [list_kbd release];
}
- return (NSString *)output;
+ keyboard_layout_dirty = false;
}
-DisplayServerOSX::LatinKeyboardVariant DisplayServerOSX::get_latin_keyboard_variant() const {
- _THREAD_SAFE_METHOD_
-
- static LatinKeyboardVariant layout = LATIN_KEYBOARD_QWERTY;
+int DisplayServerOSX::keyboard_get_layout_count() const {
+ if (keyboard_layout_dirty) {
+ _update_keyboard_layouts();
+ }
+ return kbd_layouts.size();
+}
+void DisplayServerOSX::keyboard_set_current_layout(int p_index) {
if (keyboard_layout_dirty) {
- layout = LATIN_KEYBOARD_QWERTY;
+ _update_keyboard_layouts();
+ }
+
+ ERR_FAIL_INDEX(p_index, kbd_layouts.size());
- CGKeyCode keys[] = { kVK_ANSI_Q, kVK_ANSI_W, kVK_ANSI_E, kVK_ANSI_R, kVK_ANSI_T, kVK_ANSI_Y };
- NSString *test = createStringForKeys(keys, 6);
+ NSString *cur_name = [NSString stringWithUTF8String:kbd_layouts[p_index].name.utf8().get_data()];
- if ([test isEqualToString:@"qwertz"]) {
- layout = LATIN_KEYBOARD_QWERTZ;
- } else if ([test isEqualToString:@"azerty"]) {
- layout = LATIN_KEYBOARD_AZERTY;
- } else if ([test isEqualToString:@"qzerty"]) {
- layout = LATIN_KEYBOARD_QZERTY;
- } else if ([test isEqualToString:@"',.pyf"]) {
- layout = LATIN_KEYBOARD_DVORAK;
- } else if ([test isEqualToString:@"xvlcwk"]) {
- layout = LATIN_KEYBOARD_NEO;
- } else if ([test isEqualToString:@"qwfpgj"]) {
- layout = LATIN_KEYBOARD_COLEMAK;
+ NSDictionary *filter_kbd = @{ (NSString *)kTISPropertyInputSourceType : (NSString *)kTISTypeKeyboardLayout };
+ NSArray *list_kbd = (NSArray *)TISCreateInputSourceList((CFDictionaryRef)filter_kbd, false);
+ for (NSUInteger i = 0; i < [list_kbd count]; i++) {
+ NSString *name = (NSString *)TISGetInputSourceProperty((TISInputSourceRef)[list_kbd objectAtIndex:i], kTISPropertyLocalizedName);
+ if ([name isEqualToString:cur_name]) {
+ TISSelectInputSource((TISInputSourceRef)[list_kbd objectAtIndex:i]);
+ break;
}
+ }
+ [list_kbd release];
- [test release];
+ NSDictionary *filter_ime = @{ (NSString *)kTISPropertyInputSourceType : (NSString *)kTISTypeKeyboardInputMode };
+ NSArray *list_ime = (NSArray *)TISCreateInputSourceList((CFDictionaryRef)filter_ime, false);
+ for (NSUInteger i = 0; i < [list_ime count]; i++) {
+ NSString *name = (NSString *)TISGetInputSourceProperty((TISInputSourceRef)[list_ime objectAtIndex:i], kTISPropertyLocalizedName);
+ if ([name isEqualToString:cur_name]) {
+ TISSelectInputSource((TISInputSourceRef)[list_ime objectAtIndex:i]);
+ break;
+ }
+ }
+ [list_ime release];
+}
+
+int DisplayServerOSX::keyboard_get_current_layout() const {
+ if (keyboard_layout_dirty) {
+ _update_keyboard_layouts();
+ }
- keyboard_layout_dirty = false;
- return layout;
+ return current_layout;
+}
+
+String DisplayServerOSX::keyboard_get_layout_language(int p_index) const {
+ if (keyboard_layout_dirty) {
+ _update_keyboard_layouts();
+ }
+
+ ERR_FAIL_INDEX_V(p_index, kbd_layouts.size(), "");
+ return kbd_layouts[p_index].code;
+}
+
+String DisplayServerOSX::keyboard_get_layout_name(int p_index) const {
+ if (keyboard_layout_dirty) {
+ _update_keyboard_layouts();
}
- return layout;
+ ERR_FAIL_INDEX_V(p_index, kbd_layouts.size(), "");
+ return kbd_layouts[p_index].name;
}
void DisplayServerOSX::_push_input(const Ref<InputEvent> &p_event) {
diff --git a/platform/osx/export/export.cpp b/platform/osx/export/export.cpp
index 784fba75ec..8905950020 100644
--- a/platform/osx/export/export.cpp
+++ b/platform/osx/export/export.cpp
@@ -55,6 +55,7 @@ class EditorExportPlatformOSX : public EditorExportPlatform {
void _fix_plist(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &plist, const String &p_binary);
void _make_icon(const Ref<Image> &p_icon, Vector<uint8_t> &p_data);
+ Error _notarize(const Ref<EditorExportPreset> &p_preset, const String &p_path);
Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path);
Error _create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name);
void _zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name);
@@ -66,6 +67,28 @@ class EditorExportPlatformOSX : public EditorExportPlatform {
bool use_codesign() const { return false; }
bool use_dmg() const { return false; }
#endif
+ bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const {
+ String pname = p_package;
+
+ if (pname.length() == 0) {
+ if (r_error) {
+ *r_error = TTR("Identifier is missing.");
+ }
+ return false;
+ }
+
+ for (int i = 0; i < pname.length(); i++) {
+ CharType c = pname[i];
+ if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.')) {
+ if (r_error) {
+ *r_error = vformat(TTR("The character '%s' is not allowed in Identifier."), String::chr(c));
+ }
+ return false;
+ }
+ }
+
+ return true;
+ }
protected:
virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features);
@@ -122,7 +145,7 @@ void EditorExportPlatformOSX::get_export_options(List<ExportOption> *r_options)
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/info"), "Made with Godot Engine"));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/icon", PROPERTY_HINT_FILE, "*.png,*.icns"), ""));
- r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/bundle_identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/signature"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/short_version"), "1.0"));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version"), "1.0"));
@@ -138,6 +161,11 @@ void EditorExportPlatformOSX::get_export_options(List<ExportOption> *r_options)
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/hardened_runtime"), true));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/entitlements", PROPERTY_HINT_GLOBAL_FILE, "*.plist"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "codesign/custom_options"), PackedStringArray()));
+
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "notarization/enable"), false));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Apple ID email"), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_password", PROPERTY_HINT_PLACEHOLDER_TEXT, "Enable two-factor authentication and provide app-specific password"), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_team_id", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide team ID if your Apple ID belongs to multiple teams"), ""));
#endif
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "texture_format/s3tc"), true));
@@ -326,8 +354,8 @@ void EditorExportPlatformOSX::_fix_plist(const Ref<EditorExportPreset> &p_preset
strnew += lines[i].replace("$name", p_binary) + "\n";
} else if (lines[i].find("$info") != -1) {
strnew += lines[i].replace("$info", p_preset->get("application/info")) + "\n";
- } else if (lines[i].find("$identifier") != -1) {
- strnew += lines[i].replace("$identifier", p_preset->get("application/identifier")) + "\n";
+ } else if (lines[i].find("$bundle_identifier") != -1) {
+ strnew += lines[i].replace("$bundle_identifier", p_preset->get("application/bundle_identifier")) + "\n";
} else if (lines[i].find("$short_version") != -1) {
strnew += lines[i].replace("$short_version", p_preset->get("application/short_version")) + "\n";
} else if (lines[i].find("$version") != -1) {
@@ -363,6 +391,52 @@ void EditorExportPlatformOSX::_fix_plist(const Ref<EditorExportPreset> &p_preset
- and then wrap it up in a DMG
**/
+Error EditorExportPlatformOSX::_notarize(const Ref<EditorExportPreset> &p_preset, const String &p_path) {
+#ifdef OSX_ENABLED
+ List<String> args;
+
+ args.push_back("altool");
+ args.push_back("--notarize-app");
+
+ args.push_back("--primary-bundle-id");
+ args.push_back(p_preset->get("application/bundle_identifier"));
+
+ args.push_back("--username");
+ args.push_back(p_preset->get("notarization/apple_id_name"));
+
+ args.push_back("--password");
+ args.push_back(p_preset->get("notarization/apple_id_password"));
+
+ args.push_back("--type");
+ args.push_back("osx");
+
+ if (p_preset->get("notarization/apple_team_id")) {
+ args.push_back("--asc-provider");
+ args.push_back(p_preset->get("notarization/apple_team_id"));
+ }
+
+ args.push_back("--file");
+ args.push_back(p_path);
+
+ String str;
+ Error err = OS::get_singleton()->execute("xcrun", args, true, nullptr, &str, nullptr, true);
+ ERR_FAIL_COND_V(err != OK, err);
+
+ print_line("altool (" + p_path + "):\n" + str);
+ if (str.find("RequestUUID") == -1) {
+ EditorNode::add_io_error("altool: " + str);
+ return FAILED;
+ } else {
+ print_line("Note: The notarization process generally takes less than an hour. When the process is completed, you'll receive an email.");
+ print_line(" You can check progress manually by opening a Terminal and running the following command:");
+ print_line(" \"xcrun altool --notarization-history 0 -u <your email> -p <app-specific pwd>\"");
+ }
+
+#endif
+
+ return OK;
+}
+
Error EditorExportPlatformOSX::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path) {
#ifdef OSX_ENABLED
List<String> args;
@@ -399,7 +473,7 @@ Error EditorExportPlatformOSX::_code_sign(const Ref<EditorExportPreset> &p_prese
Error err = OS::get_singleton()->execute("codesign", args, true, nullptr, &str, nullptr, true);
ERR_FAIL_COND_V(err != OK, err);
- print_line("codesign (" + p_path + "): " + str);
+ print_line("codesign (" + p_path + "):\n" + str);
if (str.find("no identity found") != -1) {
EditorNode::add_io_error("codesign: no identity found");
return FAILED;
@@ -714,6 +788,14 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
}
}
+ bool noto_enabled = p_preset->get("notarization/enable");
+ if (err == OK && noto_enabled) {
+ if (ep.step("Sending archive for notarization", 4)) {
+ return ERR_SKIP;
+ }
+ err = _notarize(p_preset, p_path);
+ }
+
// Clean up temporary .app dir.
OS::get_singleton()->move_to_trash(tmp_app_path_name);
}
@@ -803,6 +885,41 @@ bool EditorExportPlatformOSX::can_export(const Ref<EditorExportPreset> &p_preset
valid = dvalid || rvalid;
r_missing_templates = !valid;
+ String identifier = p_preset->get("application/bundle_identifier");
+ String pn_err;
+ if (!is_package_name_valid(identifier, &pn_err)) {
+ err += TTR("Invalid bundle identifier:") + " " + pn_err + "\n";
+ valid = false;
+ }
+
+ bool sign_enabled = p_preset->get("codesign/enable");
+ if (sign_enabled) {
+ if (p_preset->get("codesign/identity") == "") {
+ err += TTR("Codesign: identity not specified.") + "\n";
+ valid = false;
+ }
+ }
+ bool noto_enabled = p_preset->get("notarization/enable");
+ if (noto_enabled) {
+ if (!sign_enabled) {
+ err += TTR("Notarization: code signing required.") + "\n";
+ valid = false;
+ }
+ bool hr_enabled = p_preset->get("codesign/hardened_runtime");
+ if (!hr_enabled) {
+ err += TTR("Notarization: hardened runtime required.") + "\n";
+ valid = false;
+ }
+ if (p_preset->get("notarization/apple_id_name") == "") {
+ err += TTR("Notarization: Apple ID name not specified.") + "\n";
+ valid = false;
+ }
+ if (p_preset->get("notarization/apple_id_password") == "") {
+ err += TTR("Notarization: Apple ID password not specified.") + "\n";
+ valid = false;
+ }
+ }
+
if (!err.empty()) {
r_error = err;
}
diff --git a/platform/osx/joypad_osx.cpp b/platform/osx/joypad_osx.cpp
index d342d30097..cfc371710b 100644
--- a/platform/osx/joypad_osx.cpp
+++ b/platform/osx/joypad_osx.cpp
@@ -311,9 +311,16 @@ bool JoypadOSX::configure_joypad(IOHIDDeviceRef p_device_ref, joypad *p_joy) {
if (refCF) {
CFNumberGetValue((CFNumberRef)refCF, kCFNumberSInt32Type, &product_id);
}
+
+ int version = 0;
+ refCF = IOHIDDeviceGetProperty(p_device_ref, CFSTR(kIOHIDVersionNumberKey));
+ if (refCF) {
+ CFNumberGetValue((CFNumberRef)refCF, kCFNumberSInt32Type, &version);
+ }
+
if (vendor && product_id) {
char uid[128];
- sprintf(uid, "%04x%08x%04x%08x", OSSwapHostToBigInt32(vendor), 0, OSSwapHostToBigInt32(product_id), 0);
+ sprintf(uid, "%08x%08x%08x%08x", OSSwapHostToBigInt32(3), OSSwapHostToBigInt32(vendor), OSSwapHostToBigInt32(product_id), OSSwapHostToBigInt32(version));
input->joy_connection_changed(id, true, name, uid);
} else {
//bluetooth device
diff --git a/platform/uwp/export/export.cpp b/platform/uwp/export/export.cpp
index db42908f89..0fd017f96e 100644
--- a/platform/uwp/export/export.cpp
+++ b/platform/uwp/export/export.cpp
@@ -743,23 +743,7 @@ class EditorExportPlatformUWP : public EditorExportPlatform {
// TODO: Add resource creation or image rescaling to enable other scales:
// 1.25, 1.5, 2.0
- real_t scales[] = { 1.0 };
- bool valid_w = false;
- bool valid_h = false;
-
- for (int i = 0; i < 1; i++) {
- int w = ceil(p_width * scales[i]);
- int h = ceil(p_height * scales[i]);
-
- if (w == p_image->get_width()) {
- valid_w = true;
- }
- if (h == p_image->get_height()) {
- valid_h = true;
- }
- }
-
- return valid_w && valid_h;
+ return p_width == p_image->get_width() && p_height == p_image->get_height();
}
Vector<uint8_t> _fix_manifest(const Ref<EditorExportPreset> &p_preset, const Vector<uint8_t> &p_template, bool p_give_internet) const {
@@ -986,7 +970,7 @@ class EditorExportPlatformUWP : public EditorExportPlatform {
public:
virtual String get_name() const {
- return "Windows Universal";
+ return "UWP";
}
virtual String get_os_name() const {
return "UWP";
@@ -1196,7 +1180,7 @@ public:
virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) {
String src_appx;
- EditorProgress ep("export", "Exporting for Windows Universal", 7, true);
+ EditorProgress ep("export", "Exporting for UWP", 7, true);
if (p_debug) {
src_appx = p_preset->get("custom_template/debug");
diff --git a/platform/uwp/os_uwp.cpp b/platform/uwp/os_uwp.cpp
index 7bd67d3726..ee25754704 100644
--- a/platform/uwp/os_uwp.cpp
+++ b/platform/uwp/os_uwp.cpp
@@ -535,11 +535,28 @@ void OS_UWP::delay_usec(uint32_t p_usec) const {
uint64_t OS_UWP::get_ticks_usec() const {
uint64_t ticks;
- uint64_t time;
+
// This is the number of clock ticks since start
QueryPerformanceCounter((LARGE_INTEGER *)&ticks);
+
// Divide by frequency to get the time in seconds
- time = ticks * 1000000L / ticks_per_second;
+ // original calculation shown below is subject to overflow
+ // with high ticks_per_second and a number of days since the last reboot.
+ // time = ticks * 1000000L / ticks_per_second;
+
+ // we can prevent this by either using 128 bit math
+ // or separating into a calculation for seconds, and the fraction
+ uint64_t seconds = ticks / ticks_per_second;
+
+ // compiler will optimize these two into one divide
+ uint64_t leftover = ticks % ticks_per_second;
+
+ // remainder
+ uint64_t time = (leftover * 1000000L) / ticks_per_second;
+
+ // seconds
+ time += seconds * 1000000L;
+
// Subtract the time at game start to get
// the time since the game started
time -= ticks_start;
diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp
index 9d8344fa7e..7cb6e90edb 100644
--- a/platform/windows/display_server_windows.cpp
+++ b/platform/windows/display_server_windows.cpp
@@ -1372,70 +1372,99 @@ void DisplayServerWindows::enable_for_stealing_focus(OS::ProcessID pid) {
AllowSetForegroundWindow(pid);
}
-DisplayServer::LatinKeyboardVariant DisplayServerWindows::get_latin_keyboard_variant() const {
- _THREAD_SAFE_METHOD_
+int DisplayServerWindows::keyboard_get_layout_count() const {
+ return GetKeyboardLayoutList(0, NULL);
+}
- unsigned long azerty[] = {
- 0x00020401, // Arabic (102) AZERTY
- 0x0001080c, // Belgian (Comma)
- 0x0000080c, // Belgian French
- 0x0000040c, // French
- 0 // <--- STOP MARK
- };
- unsigned long qwertz[] = {
- 0x0000041a, // Croation
- 0x00000405, // Czech
- 0x00000407, // German
- 0x00010407, // German (IBM)
- 0x0000040e, // Hungarian
- 0x0000046e, // Luxembourgish
- 0x00010415, // Polish (214)
- 0x00000418, // Romanian (Legacy)
- 0x0000081a, // Serbian (Latin)
- 0x0000041b, // Slovak
- 0x00000424, // Slovenian
- 0x0001042e, // Sorbian Extended
- 0x0002042e, // Sorbian Standard
- 0x0000042e, // Sorbian Standard (Legacy)
- 0x0000100c, // Swiss French
- 0x00000807, // Swiss German
- 0 // <--- STOP MARK
- };
- unsigned long dvorak[] = {
- 0x00010409, // US-Dvorak
- 0x00030409, // US-Dvorak for left hand
- 0x00040409, // US-Dvorak for right hand
- 0 // <--- STOP MARK
- };
+int DisplayServerWindows::keyboard_get_current_layout() const {
+ HKL cur_layout = GetKeyboardLayout(0);
+
+ int layout_count = GetKeyboardLayoutList(0, NULL);
+ HKL *layouts = (HKL *)memalloc(layout_count * sizeof(HKL));
+ GetKeyboardLayoutList(layout_count, layouts);
+
+ for (int i = 0; i < layout_count; i++) {
+ if (cur_layout == layouts[i]) {
+ memfree(layouts);
+ return i;
+ }
+ }
+ memfree(layouts);
+ return -1;
+}
+
+void DisplayServerWindows::keyboard_set_current_layout(int p_index) {
+ int layout_count = GetKeyboardLayoutList(0, NULL);
+
+ ERR_FAIL_INDEX(p_index, layout_count);
+
+ HKL *layouts = (HKL *)memalloc(layout_count * sizeof(HKL));
+ GetKeyboardLayoutList(layout_count, layouts);
+ ActivateKeyboardLayout(layouts[p_index], KLF_SETFORPROCESS);
+ memfree(layouts);
+}
+
+String DisplayServerWindows::keyboard_get_layout_language(int p_index) const {
+ int layout_count = GetKeyboardLayoutList(0, NULL);
- char name[KL_NAMELENGTH + 1];
- name[0] = 0;
- GetKeyboardLayoutNameA(name);
+ ERR_FAIL_INDEX_V(p_index, layout_count, "");
- unsigned long hex = strtoul(name, nullptr, 16);
+ HKL *layouts = (HKL *)memalloc(layout_count * sizeof(HKL));
+ GetKeyboardLayoutList(layout_count, layouts);
- int i = 0;
- while (azerty[i] != 0) {
- if (azerty[i] == hex)
- return LATIN_KEYBOARD_AZERTY;
- i++;
+ wchar_t buf[LOCALE_NAME_MAX_LENGTH];
+ memset(buf, 0, LOCALE_NAME_MAX_LENGTH * sizeof(wchar_t));
+ LCIDToLocaleName(MAKELCID(LOWORD(layouts[p_index]), SORT_DEFAULT), buf, LOCALE_NAME_MAX_LENGTH, 0);
+
+ memfree(layouts);
+
+ return String(buf).substr(0, 2);
+}
+
+String _get_full_layout_name_from_registry(HKL p_layout) {
+ String id = "SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\" + String::num_int64((int64_t)p_layout, 16, false).lpad(8, "0");
+ String ret;
+
+ HKEY hkey;
+ wchar_t layout_text[1024];
+ memset(layout_text, 0, 1024 * sizeof(wchar_t));
+
+ if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, (LPCWSTR)id.c_str(), 0, KEY_QUERY_VALUE, &hkey) != ERROR_SUCCESS) {
+ return ret;
}
- i = 0;
- while (qwertz[i] != 0) {
- if (qwertz[i] == hex)
- return LATIN_KEYBOARD_QWERTZ;
- i++;
+ DWORD buffer = 1024;
+ DWORD vtype = REG_SZ;
+ if (RegQueryValueExW(hkey, L"Layout Text", NULL, &vtype, (LPBYTE)layout_text, &buffer) == ERROR_SUCCESS) {
+ ret = String(layout_text);
}
+ RegCloseKey(hkey);
+ return ret;
+}
+
+String DisplayServerWindows::keyboard_get_layout_name(int p_index) const {
+ int layout_count = GetKeyboardLayoutList(0, NULL);
+
+ ERR_FAIL_INDEX_V(p_index, layout_count, "");
+
+ HKL *layouts = (HKL *)memalloc(layout_count * sizeof(HKL));
+ GetKeyboardLayoutList(layout_count, layouts);
- i = 0;
- while (dvorak[i] != 0) {
- if (dvorak[i] == hex)
- return LATIN_KEYBOARD_DVORAK;
- i++;
+ String ret = _get_full_layout_name_from_registry(layouts[p_index]); // Try reading full name from Windows registry, fallback to locale name if failed (e.g. on Wine).
+ if (ret == String()) {
+ wchar_t buf[LOCALE_NAME_MAX_LENGTH];
+ memset(buf, 0, LOCALE_NAME_MAX_LENGTH * sizeof(wchar_t));
+ LCIDToLocaleName(MAKELCID(LOWORD(layouts[p_index]), SORT_DEFAULT), buf, LOCALE_NAME_MAX_LENGTH, 0);
+
+ wchar_t name[1024];
+ memset(name, 0, 1024 * sizeof(wchar_t));
+ GetLocaleInfoEx(buf, LOCALE_SLOCALIZEDDISPLAYNAME, (LPWSTR)&name, 1024);
+
+ ret = String(name);
}
+ memfree(layouts);
- return LATIN_KEYBOARD_QWERTY;
+ return ret;
}
void DisplayServerWindows::process_events() {
@@ -2536,9 +2565,9 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
case WM_KEYUP:
case WM_KEYDOWN: {
if (wParam == VK_SHIFT)
- shift_mem = uMsg == WM_KEYDOWN;
+ shift_mem = (uMsg == WM_KEYDOWN || uMsg == WM_SYSKEYDOWN);
if (wParam == VK_CONTROL)
- control_mem = uMsg == WM_KEYDOWN;
+ control_mem = (uMsg == WM_KEYDOWN || uMsg == WM_SYSKEYDOWN);
if (wParam == VK_MENU) {
alt_mem = (uMsg == WM_KEYDOWN || uMsg == WM_SYSKEYDOWN);
if (lParam & (1 << 24))
diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h
index caf8598dc2..995ced0809 100644
--- a/platform/windows/display_server_windows.h
+++ b/platform/windows/display_server_windows.h
@@ -523,7 +523,11 @@ public:
virtual void enable_for_stealing_focus(OS::ProcessID pid);
- virtual LatinKeyboardVariant get_latin_keyboard_variant() const;
+ virtual int keyboard_get_layout_count() const;
+ virtual int keyboard_get_current_layout() const;
+ virtual void keyboard_set_current_layout(int p_index);
+ virtual String keyboard_get_layout_language(int p_index) const;
+ virtual String keyboard_get_layout_name(int p_index) const;
virtual void process_events();
diff --git a/platform/windows/joypad_windows.cpp b/platform/windows/joypad_windows.cpp
index 0cff12ca8c..271a4e41bc 100644
--- a/platform/windows/joypad_windows.cpp
+++ b/platform/windows/joypad_windows.cpp
@@ -33,10 +33,6 @@
#include <oleauto.h>
#include <wbemidl.h>
-#ifndef __GNUC__
-#define __builtin_bswap32 _byteswap_ulong
-#endif
-
#if defined(__GNUC__)
// Workaround GCC warning from -Wcast-function-type.
#define GetProcAddress (void *)GetProcAddress
@@ -67,17 +63,25 @@ JoypadWindows::JoypadWindows(HWND *hwnd) {
for (int i = 0; i < JOYPADS_MAX; i++)
attached_joypads[i] = false;
- HRESULT result;
- result = DirectInput8Create(GetModuleHandle(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8, (void **)&dinput, nullptr);
- if (FAILED(result)) {
- printf("failed init DINPUT: %ld\n", result);
+ HRESULT result = DirectInput8Create(GetModuleHandle(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8, (void **)&dinput, nullptr);
+ if (result == DI_OK) {
+ probe_joypads();
+ } else {
+ ERR_PRINT("Couldn't initialize DirectInput. Error: " + itos(result));
+ if (result == DIERR_OUTOFMEMORY) {
+ ERR_PRINT("The Windows DirectInput subsystem could not allocate sufficient memory.");
+ ERR_PRINT("Rebooting your PC may solve this issue.");
+ }
+ // Ensure dinput is still a nullptr.
+ dinput = nullptr;
}
- probe_joypads();
}
JoypadWindows::~JoypadWindows() {
close_joypad();
- dinput->Release();
+ if (dinput) {
+ dinput->Release();
+ }
unload_xinput();
}
@@ -135,6 +139,7 @@ bool JoypadWindows::is_xinput_device(const GUID *p_guid) {
}
bool JoypadWindows::setup_dinput_joypad(const DIDEVICEINSTANCE *instance) {
+ ERR_FAIL_NULL_V_MSG(dinput, false, "DirectInput not initialized. Rebooting your PC may solve this issue.");
HRESULT hr;
int num = input->get_unused_joy_id();
@@ -158,10 +163,13 @@ bool JoypadWindows::setup_dinput_joypad(const DIDEVICEINSTANCE *instance) {
const GUID &guid = instance->guidProduct;
char uid[128];
- sprintf_s(uid, "%08lx%04hx%04hx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx",
- __builtin_bswap32(guid.Data1), guid.Data2, guid.Data3,
- guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3],
- guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]);
+
+ ERR_FAIL_COND_V_MSG(memcmp(&guid.Data4[2], "PIDVID", 6), false, "DirectInput device not recognised.");
+ WORD type = BSWAP16(0x03);
+ WORD vendor = BSWAP16(LOWORD(guid.Data1));
+ WORD product = BSWAP16(HIWORD(guid.Data1));
+ WORD version = 0;
+ sprintf_s(uid, "%04x%04x%04x%04x%04x%04x%04x%04x", type, 0, vendor, 0, product, 0, version, 0);
id_to_change = joypad_count;
@@ -266,6 +274,7 @@ void JoypadWindows::close_joypad(int id) {
}
void JoypadWindows::probe_joypads() {
+ ERR_FAIL_NULL_MSG(dinput, "DirectInput not initialized. Rebooting your PC may solve this issue.");
DWORD dwResult;
for (DWORD i = 0; i < XUSER_MAX_COUNT; i++) {
ZeroMemory(&x_joypads[i].state, sizeof(XINPUT_STATE));
diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp
index f11888b26c..5b15896b0c 100644
--- a/platform/windows/os_windows.cpp
+++ b/platform/windows/os_windows.cpp
@@ -208,6 +208,13 @@ void OS_Windows::initialize() {
process_map = memnew((Map<ProcessID, ProcessInfo>));
+ // Add current Godot PID to the list of known PIDs
+ ProcessInfo current_pi = {};
+ PROCESS_INFORMATION current_pi_pi = {};
+ current_pi.pi = current_pi_pi;
+ current_pi.pi.hProcess = GetCurrentProcess();
+ process_map->insert(GetCurrentProcessId(), current_pi);
+
IP_Unix::make_default();
main_loop = nullptr;
}
@@ -343,55 +350,21 @@ OS::TimeZoneInfo OS_Windows::get_time_zone_info() const {
return ret;
}
-uint64_t OS_Windows::get_unix_time() const {
- FILETIME ft;
- SYSTEMTIME st;
- GetSystemTime(&st);
- SystemTimeToFileTime(&st, &ft);
-
- SYSTEMTIME ep;
- ep.wYear = 1970;
- ep.wMonth = 1;
- ep.wDayOfWeek = 4;
- ep.wDay = 1;
- ep.wHour = 0;
- ep.wMinute = 0;
- ep.wSecond = 0;
- ep.wMilliseconds = 0;
- FILETIME fep;
- SystemTimeToFileTime(&ep, &fep);
-
- // Type punning through unions (rather than pointer cast) as per:
- // https://docs.microsoft.com/en-us/windows/desktop/api/minwinbase/ns-minwinbase-filetime#remarks
- ULARGE_INTEGER ft_punning;
- ft_punning.LowPart = ft.dwLowDateTime;
- ft_punning.HighPart = ft.dwHighDateTime;
-
- ULARGE_INTEGER fep_punning;
- fep_punning.LowPart = fep.dwLowDateTime;
- fep_punning.HighPart = fep.dwHighDateTime;
-
- return (ft_punning.QuadPart - fep_punning.QuadPart) / 10000000;
-};
-
-uint64_t OS_Windows::get_system_time_secs() const {
- return get_system_time_msecs() / 1000;
-}
-
-uint64_t OS_Windows::get_system_time_msecs() const {
- const uint64_t WINDOWS_TICK = 10000;
- const uint64_t MSEC_TO_UNIX_EPOCH = 11644473600000LL;
+double OS_Windows::get_unix_time() const {
+ // 1 Windows tick is 100ns
+ const uint64_t WINDOWS_TICKS_PER_SECOND = 10000000;
+ const uint64_t TICKS_TO_UNIX_EPOCH = 116444736000000000LL;
SYSTEMTIME st;
GetSystemTime(&st);
FILETIME ft;
SystemTimeToFileTime(&st, &ft);
- uint64_t ret;
- ret = ft.dwHighDateTime;
- ret <<= 32;
- ret |= ft.dwLowDateTime;
+ uint64_t ticks_time;
+ ticks_time = ft.dwHighDateTime;
+ ticks_time <<= 32;
+ ticks_time |= ft.dwLowDateTime;
- return (uint64_t)(ret / WINDOWS_TICK - MSEC_TO_UNIX_EPOCH);
+ return (double)(ticks_time - TICKS_TO_UNIX_EPOCH) / WINDOWS_TICKS_PER_SECOND;
}
void OS_Windows::delay_usec(uint32_t p_usec) const {
@@ -403,12 +376,29 @@ void OS_Windows::delay_usec(uint32_t p_usec) const {
uint64_t OS_Windows::get_ticks_usec() const {
uint64_t ticks;
- uint64_t time;
+
// This is the number of clock ticks since start
if (!QueryPerformanceCounter((LARGE_INTEGER *)&ticks))
ticks = (UINT64)timeGetTime();
+
// Divide by frequency to get the time in seconds
- time = ticks * 1000000L / ticks_per_second;
+ // original calculation shown below is subject to overflow
+ // with high ticks_per_second and a number of days since the last reboot.
+ // time = ticks * 1000000L / ticks_per_second;
+
+ // we can prevent this by either using 128 bit math
+ // or separating into a calculation for seconds, and the fraction
+ uint64_t seconds = ticks / ticks_per_second;
+
+ // compiler will optimize these two into one divide
+ uint64_t leftover = ticks % ticks_per_second;
+
+ // remainder
+ uint64_t time = (leftover * 1000000L) / ticks_per_second;
+
+ // seconds
+ time += seconds * 1000000L;
+
// Subtract the time at game start to get
// the time since the game started
time -= ticks_start;
@@ -795,6 +785,9 @@ String OS_Windows::get_current_tablet_driver() const {
}
void OS_Windows::set_current_tablet_driver(const String &p_driver) {
+ if (get_tablet_driver_count() == 0) {
+ return;
+ }
bool found = false;
for (int i = 0; i < get_tablet_driver_count(); i++) {
if (p_driver == get_tablet_driver_name(i)) {
diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h
index 11e3533bfd..910a83539a 100644
--- a/platform/windows/os_windows.h
+++ b/platform/windows/os_windows.h
@@ -129,9 +129,7 @@ public:
virtual Date get_date(bool utc) const;
virtual Time get_time(bool utc) const;
virtual TimeZoneInfo get_time_zone_info() const;
- virtual uint64_t get_unix_time() const;
- virtual uint64_t get_system_time_secs() const;
- virtual uint64_t get_system_time_msecs() const;
+ virtual double get_unix_time() const;
virtual Error set_cwd(const String &p_cwd);