Browse Source

Re-architecture of the Godot Android plugin.

fhuya 5 years ago
parent
commit
c3660bb4dc
30 changed files with 2226 additions and 1110 deletions
  1. 4 0
      .editorconfig
  2. 1 1
      .gitignore
  3. 12 8
      editor/export_template_manager.cpp
  4. 2 0
      platform/android/SCsub
  5. 12 0
      platform/android/export/export.cpp
  6. 5 0
      platform/android/java/app/AndroidManifest.xml
  7. 15 0
      platform/android/java/app/build.gradle
  8. 54 15
      platform/android/java/app/config.gradle
  9. 69 27
      platform/android/java/build.gradle
  10. 1 0
      platform/android/java/lib/build.gradle
  11. 68 6
      platform/android/java/lib/src/org/godotengine/godot/Godot.java
  12. 0 16
      platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
  13. 16 1
      platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java
  14. 97 0
      platform/android/java/lib/src/org/godotengine/godot/payments/GodotPaymentInterface.java
  15. 33 34
      platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java
  16. 4 5
      platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java
  17. 256 0
      platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java
  18. 199 0
      platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java
  19. 31 0
      platform/android/java/plugins/godotpayment/build.gradle
  20. 11 0
      platform/android/java/plugins/godotpayment/src/main/AndroidManifest.xml
  21. 59 21
      platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/GodotPayment.java
  22. 1 0
      platform/android/java/settings.gradle
  23. 406 946
      platform/android/java_godot_lib_jni.cpp
  24. 28 30
      platform/android/java_godot_lib_jni.h
  25. 10 0
      platform/android/java_godot_wrapper.cpp
  26. 2 0
      platform/android/java_godot_wrapper.h
  27. 432 0
      platform/android/jni_utils.cpp
  28. 55 0
      platform/android/jni_utils.h
  29. 300 0
      platform/android/plugin/godot_plugin_jni.cpp
  30. 43 0
      platform/android/plugin/godot_plugin_jni.h

+ 4 - 0
.editorconfig

@@ -9,6 +9,10 @@ insert_final_newline = true
 [*.{cpp,hpp,c,h,mm}]
 [*.{cpp,hpp,c,h,mm}]
 trim_trailing_whitespace = true
 trim_trailing_whitespace = true
 
 
+[{*.gradle,AndroidManifest.xml}]
+indent_style = space
+indent_size = 4
+
 [{*.{py,cs},SConstruct,SCsub}]
 [{*.{py,cs},SConstruct,SCsub}]
 indent_style = space
 indent_style = space
 indent_size = 4
 indent_size = 4

+ 1 - 1
.gitignore

@@ -19,7 +19,7 @@ local.properties
 .gradletasknamecache
 .gradletasknamecache
 project.properties
 project.properties
 platform/android/java/libs/*
 platform/android/java/libs/*
-platform/android/java/assets
+platform/android/java/app/libs/*
 
 
 # General c++ generated files
 # General c++ generated files
 *.lib
 *.lib

+ 12 - 8
editor/export_template_manager.cpp

@@ -560,13 +560,6 @@ Error ExportTemplateManager::install_android_template() {
 
 
 	// Make res://android dir (if it does not exist).
 	// Make res://android dir (if it does not exist).
 	da->make_dir("android");
 	da->make_dir("android");
-	{
-		// Add an empty .gdignore file to avoid scan.
-		FileAccessRef f = FileAccess::open("res://android/.gdignore", FileAccess::WRITE);
-		ERR_FAIL_COND_V(!f, ERR_CANT_CREATE);
-		f->store_line("");
-		f->close();
-	}
 	{
 	{
 		// Add version, to ensure building won't work if template and Godot version don't match.
 		// Add version, to ensure building won't work if template and Godot version don't match.
 		FileAccessRef f = FileAccess::open("res://android/.build_version", FileAccess::WRITE);
 		FileAccessRef f = FileAccess::open("res://android/.build_version", FileAccess::WRITE);
@@ -575,8 +568,19 @@ Error ExportTemplateManager::install_android_template() {
 		f->close();
 		f->close();
 	}
 	}
 
 
-	Error err = da->make_dir_recursive("android/build");
+	// Create the android plugins directory.
+	Error err = da->make_dir_recursive("android/plugins");
+	ERR_FAIL_COND_V(err != OK, err);
+
+	err = da->make_dir_recursive("android/build");
 	ERR_FAIL_COND_V(err != OK, err);
 	ERR_FAIL_COND_V(err != OK, err);
+	{
+		// Add an empty .gdignore file to avoid scan.
+		FileAccessRef f = FileAccess::open("res://android/build/.gdignore", FileAccess::WRITE);
+		ERR_FAIL_COND_V(!f, ERR_CANT_CREATE);
+		f->store_line("");
+		f->close();
+	}
 
 
 	// Uncompress source template.
 	// Uncompress source template.
 
 

+ 2 - 0
platform/android/SCsub

@@ -18,6 +18,8 @@ android_files = [
     'java_class_wrapper.cpp',
     'java_class_wrapper.cpp',
     'java_godot_wrapper.cpp',
     'java_godot_wrapper.cpp',
     'java_godot_io_wrapper.cpp',
     'java_godot_io_wrapper.cpp',
+    'jni_utils.cpp',
+    'plugin/godot_plugin_jni.cpp',
     #'power_android.cpp'
     #'power_android.cpp'
 ]
 ]
 
 

+ 12 - 0
platform/android/export/export.cpp

@@ -692,6 +692,8 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
 
 
 		int xr_mode_index = p_preset->get("xr_features/xr_mode");
 		int xr_mode_index = p_preset->get("xr_features/xr_mode");
 
 
+		String plugins = p_preset->get("custom_template/plugins");
+
 		Vector<String> perms;
 		Vector<String> perms;
 
 
 		const char **aperms = android_perms;
 		const char **aperms = android_perms;
@@ -860,6 +862,11 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
 							}
 							}
 						}
 						}
 
 
+						if (tname == "meta-data" && attrname == "value" && value == "custom_template_plugins_value") {
+							// Update the meta-data 'android:value' attribute with the list of enabled plugins.
+							string_table.write[attr_value] = plugins;
+						}
+
 						iofs += 20;
 						iofs += 20;
 					}
 					}
 
 
@@ -1373,6 +1380,7 @@ public:
 		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/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));
 		r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));
 		r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));
 		r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "custom_template/use_custom_build"), false));
 		r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "custom_template/use_custom_build"), false));
+		r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/plugins", PROPERTY_HINT_PLACEHOLDER_TEXT, "Plugin1,Plugin2,..."), ""));
 		r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "command_line/extra_args"), ""));
 		r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "command_line/extra_args"), ""));
 		r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "version/code", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 1));
 		r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "version/code", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 1));
 		r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "version/name"), "1.0"));
 		r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "version/name"), "1.0"));
@@ -2083,14 +2091,18 @@ public:
 #endif
 #endif
 
 
 			String build_path = ProjectSettings::get_singleton()->get_resource_path().plus_file("android/build");
 			String build_path = ProjectSettings::get_singleton()->get_resource_path().plus_file("android/build");
+			String plugins_dir = ProjectSettings::get_singleton()->get_resource_path().plus_file("android/plugins");
 
 
 			build_command = build_path.plus_file(build_command);
 			build_command = build_path.plus_file(build_command);
 
 
 			String package_name = get_package_name(p_preset->get("package/unique_name"));
 			String package_name = get_package_name(p_preset->get("package/unique_name"));
+			String plugins = p_preset->get("custom_template/plugins");
 
 
 			List<String> cmdline;
 			List<String> cmdline;
 			cmdline.push_back("build");
 			cmdline.push_back("build");
 			cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name.
 			cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name.
+			cmdline.push_back("-Pcustom_template_plugins_dir=" + plugins_dir); // argument to specify the plugins directory.
+			cmdline.push_back("-Pcustom_template_plugins=" + plugins); // argument to specify the list of plugins to enable.
 			cmdline.push_back("-p"); // argument to specify the start directory.
 			cmdline.push_back("-p"); // argument to specify the start directory.
 			cmdline.push_back(build_path); // start directory.
 			cmdline.push_back(build_path); // start directory.
 			/*{ used for debug
 			/*{ used for debug

+ 5 - 0
platform/android/java/app/AndroidManifest.xml

@@ -37,6 +37,11 @@
             android:name="xr_mode_metadata_name"
             android:name="xr_mode_metadata_name"
             android:value="xr_mode_metadata_value" />
             android:value="xr_mode_metadata_value" />
 
 
+        <!-- Metadata populated at export time and used by Godot to figure out which plugins must be enabled. -->
+        <meta-data
+            android:name="custom_template_plugins"
+            android:value="custom_template_plugins_value"/>
+
         <activity
         <activity
             android:name=".GodotApp"
             android:name=".GodotApp"
             android:label="@string/godot_project_name_string"
             android:label="@string/godot_project_name_string"

+ 15 - 0
platform/android/java/app/build.gradle

@@ -34,15 +34,30 @@ allprojects {
 
 
 dependencies {
 dependencies {
     implementation libraries.supportCoreUtils
     implementation libraries.supportCoreUtils
+    implementation libraries.v4Support
 
 
     if (rootProject.findProject(":lib")) {
     if (rootProject.findProject(":lib")) {
         implementation project(":lib")
         implementation project(":lib")
+    } else if (rootProject.findProject(":godot:lib")) {
+        implementation project(":godot:lib")
     } else {
     } else {
         // Custom build mode. In this scenario this project is the only one around and the Godot
         // Custom build mode. In this scenario this project is the only one around and the Godot
         // library is available through the pre-generated godot-lib.*.aar android archive files.
         // library is available through the pre-generated godot-lib.*.aar android archive files.
         debugImplementation fileTree(dir: 'libs/debug', include: ['*.jar', '*.aar'])
         debugImplementation fileTree(dir: 'libs/debug', include: ['*.jar', '*.aar'])
         releaseImplementation fileTree(dir: 'libs/release', include: ['*.jar', '*.aar'])
         releaseImplementation fileTree(dir: 'libs/release', include: ['*.jar', '*.aar'])
     }
     }
+
+    // Godot prebuilt plugins
+    implementation fileTree(dir: 'libs/plugins', include: ["GodotPayment*.aar"])
+
+    // Godot user plugins dependencies
+    String pluginsDir = getGodotPluginsDirectory()
+    String[] pluginsBinaries = getGodotPluginsBinaries()
+    if (pluginsDir != null && !pluginsDir.isEmpty() &&
+        pluginsBinaries != null && pluginsBinaries.size() > 0) {
+        implementation fileTree(dir: pluginsDir, include: pluginsBinaries)
+    }
+
 //CHUNK_DEPENDENCIES_BEGIN
 //CHUNK_DEPENDENCIES_BEGIN
 //CHUNK_DEPENDENCIES_END
 //CHUNK_DEPENDENCIES_END
 }
 }

+ 54 - 15
platform/android/java/app/config.gradle

@@ -1,24 +1,63 @@
 ext.versions = [
 ext.versions = [
-        androidGradlePlugin : '3.4.2',
-        compileSdk : 28,
-        minSdk : 18,
-        targetSdk : 28,
-        buildTools : '28.0.3',
-        supportCoreUtils : '28.0.0'
+    androidGradlePlugin: '3.4.2',
+    compileSdk         : 28,
+    minSdk             : 18,
+    targetSdk          : 28,
+    buildTools         : '28.0.3',
+    supportCoreUtils   : '28.0.0',
+    v4Support          : '28.0.0'
 
 
 ]
 ]
 
 
 ext.libraries = [
 ext.libraries = [
-        androidGradlePlugin : "com.android.tools.build:gradle:$versions.androidGradlePlugin",
-        supportCoreUtils : "com.android.support:support-core-utils:$versions.supportCoreUtils"
+    androidGradlePlugin: "com.android.tools.build:gradle:$versions.androidGradlePlugin",
+    supportCoreUtils   : "com.android.support:support-core-utils:$versions.supportCoreUtils",
+    v4Support          : "com.android.support:support-v4:$versions.v4Support"
 ]
 ]
 
 
 ext.getExportPackageName = { ->
 ext.getExportPackageName = { ->
-	// Retrieve the app id from the project property set by the Godot build command.
-	String appId = project.hasProperty("export_package_name") ? project.property("export_package_name") : ""
-	// Check if the app id is valid, otherwise use the default.
-	if (appId == null || appId.isEmpty()) {
-		appId = "com.godot.game"
-	}
-	return appId
+    // Retrieve the app id from the project property set by the Godot build command.
+    String appId = project.hasProperty("export_package_name") ? project.property("export_package_name") : ""
+    // Check if the app id is valid, otherwise use the default.
+    if (appId == null || appId.isEmpty()) {
+        appId = "com.godot.game"
+    }
+    return appId
+}
+
+/**
+ * Parse the project properties for the 'custom_template_plugins' property and return
+ * their binaries for inclusion in the build dependencies.
+ *
+ * The listed plugins must have their binaries in the project plugins directory.
+ */
+ext.getGodotPluginsBinaries = { ->
+    String[] binDeps = []
+
+    // Retrieve the list of enabled plugins.
+    if (project.hasProperty("custom_template_plugins")) {
+        String pluginsList = project.property("custom_template_plugins")
+        if (pluginsList != null && !pluginsList.trim().isEmpty()) {
+            for (String plugin : pluginsList.split(",")) {
+                binDeps += plugin.trim() + "*.aar"
+            }
+        }
+    }
+
+    return binDeps
+}
+
+/**
+ * Parse the project properties for the 'custom_template_plugins_dir' property and return
+ * its value.
+ *
+ * The returned value is the directory containing user plugins.
+ */
+ext.getGodotPluginsDirectory = { ->
+    // The plugins directory is provided by the 'custom_template_plugins_dir' property.
+    String pluginsDir = project.hasProperty("custom_template_plugins_dir")
+        ? project.property("custom_template_plugins_dir")
+        : ""
+
+    return pluginsDir
 }
 }

+ 69 - 27
platform/android/java/build.gradle

@@ -24,7 +24,7 @@ ext {
     sconsExt = org.gradle.internal.os.OperatingSystem.current().isWindows() ? ".bat" : ""
     sconsExt = org.gradle.internal.os.OperatingSystem.current().isWindows() ? ".bat" : ""
 
 
     supportedAbis = ["armv7", "arm64v8", "x86", "x86_64"]
     supportedAbis = ["armv7", "arm64v8", "x86", "x86_64"]
-    supportedTargets = ['release':"release", 'debug':"release_debug"]
+    supportedTargets = ['release': "release", 'debug': "release_debug"]
 
 
     // Used by gradle to specify which architecture to build for by default when running `./gradlew build`.
     // Used by gradle to specify which architecture to build for by default when running `./gradlew build`.
     // This command is usually used by Android Studio.
     // This command is usually used by Android Studio.
@@ -64,10 +64,10 @@ task copyReleaseBinaryToBin(type: Copy) {
 }
 }
 
 
 /**
 /**
- * Copy the Godot android library archive debug file into the app debug libs directory.
+ * Copy the Godot android library archive debug file into the app module debug libs directory.
  * Depends on the library build task to ensure the AAR file is generated prior to copying.
  * Depends on the library build task to ensure the AAR file is generated prior to copying.
  */
  */
-task copyDebugAAR(type: Copy) {
+task copyDebugAARToAppModule(type: Copy) {
     dependsOn ':lib:assembleDebug'
     dependsOn ':lib:assembleDebug'
     from('lib/build/outputs/aar')
     from('lib/build/outputs/aar')
     into('app/libs/debug')
     into('app/libs/debug')
@@ -75,16 +75,45 @@ task copyDebugAAR(type: Copy) {
 }
 }
 
 
 /**
 /**
- * Copy the Godot android library archive release file into the app release libs directory.
+ * Copy the Godot android library archive debug file into the root bin directory.
  * Depends on the library build task to ensure the AAR file is generated prior to copying.
  * Depends on the library build task to ensure the AAR file is generated prior to copying.
  */
  */
-task copyReleaseAAR(type: Copy) {
+task copyDebugAARToBin(type: Copy) {
+    dependsOn ':lib:assembleDebug'
+    from('lib/build/outputs/aar')
+    into(binDir)
+    include('godot-lib.debug.aar')
+}
+
+/**
+ * Copy the Godot android library archive release file into the app module release libs directory.
+ * Depends on the library build task to ensure the AAR file is generated prior to copying.
+ */
+task copyReleaseAARToAppModule(type: Copy) {
     dependsOn ':lib:assembleRelease'
     dependsOn ':lib:assembleRelease'
     from('lib/build/outputs/aar')
     from('lib/build/outputs/aar')
     into('app/libs/release')
     into('app/libs/release')
     include('godot-lib.release.aar')
     include('godot-lib.release.aar')
 }
 }
 
 
+task copyGodotPaymentPluginToAppModule(type: Copy) {
+    dependsOn ':plugins:godotpayment:assembleRelease'
+    from('plugins/godotpayment/build/outputs/aar')
+    into('app/libs/plugins')
+    include('GodotPayment.release.aar')
+}
+
+/**
+ * Copy the Godot android library archive release file into the root bin directory.
+ * Depends on the library build task to ensure the AAR file is generated prior to copying.
+ */
+task copyReleaseAARToBin(type: Copy) {
+    dependsOn ':lib:assembleRelease'
+    from('lib/build/outputs/aar')
+    into(binDir)
+    include('godot-lib.release.aar')
+}
+
 /**
 /**
  * Generate Godot custom build template by zipping the source files from the app directory, as well
  * Generate Godot custom build template by zipping the source files from the app directory, as well
  * as the AAR files generated by 'copyDebugAAR' and 'copyReleaseAAR'.
  * as the AAR files generated by 'copyDebugAAR' and 'copyReleaseAAR'.
@@ -95,7 +124,7 @@ task zipCustomBuild(type: Zip) {
     doFirst {
     doFirst {
         logger.lifecycle("Generating Godot custom build template")
         logger.lifecycle("Generating Godot custom build template")
     }
     }
-    from(fileTree(dir: 'app', excludes: ['**/build/**', '**/.gradle/**', '**/*.iml']), fileTree(dir: '.', includes: ['gradle.properties','gradlew', 'gradlew.bat', 'gradle/**']))
+    from(fileTree(dir: 'app', excludes: ['**/build/**', '**/.gradle/**', '**/*.iml']), fileTree(dir: '.', includes: ['gradle.properties', 'gradlew', 'gradlew.bat', 'gradle/**']))
     include '**/*'
     include '**/*'
     archiveName 'android_source.zip'
     archiveName 'android_source.zip'
     destinationDir(file(binDir))
     destinationDir(file(binDir))
@@ -110,19 +139,24 @@ task generateGodotTemplates(type: GradleBuild) {
         startParameter.excludedTaskNames += ":lib:" + getSconsTaskName(buildType)
         startParameter.excludedTaskNames += ":lib:" + getSconsTaskName(buildType)
     }
     }
 
 
-    tasks = []
+    tasks = ["copyGodotPaymentPluginToAppModule"]
 
 
     // Only build the apks and aar files for which we have native shared libraries.
     // Only build the apks and aar files for which we have native shared libraries.
     for (String target : supportedTargets.keySet()) {
     for (String target : supportedTargets.keySet()) {
         File targetLibs = new File("lib/libs/" + target)
         File targetLibs = new File("lib/libs/" + target)
-        if (targetLibs != null && targetLibs.isDirectory()) {
-            File[] targetLibsContents = targetLibs.listFiles()
-            if (targetLibsContents != null && targetLibsContents.length > 0) {
-                // Copy the generated aar library files to the custom build directory.
-                tasks += "copy" + target.capitalize() + "AAR"
-                // Copy the prebuilt binary templates to the bin directory.
-                tasks += "copy" + target.capitalize() + "BinaryToBin"
-            }
+        if (targetLibs != null
+            && targetLibs.isDirectory()
+            && targetLibs.listFiles() != null
+            && targetLibs.listFiles().length > 0) {
+            String capitalizedTarget = target.capitalize()
+            // Copy the generated aar library files to the custom build directory.
+            tasks += "copy" + capitalizedTarget + "AARToAppModule"
+            // Copy the generated aar library files to the bin directory.
+            tasks += "copy" + capitalizedTarget + "AARToBin"
+            // Copy the prebuilt binary templates to the bin directory.
+            tasks += "copy" + capitalizedTarget + "BinaryToBin"
+        } else {
+            logger.lifecycle("No native shared libs for target $target. Skipping build.")
         }
         }
     }
     }
 
 
@@ -133,20 +167,28 @@ task generateGodotTemplates(type: GradleBuild) {
  * Clean the generated artifacts.
  * Clean the generated artifacts.
  */
  */
 task cleanGodotTemplates(type: Delete) {
 task cleanGodotTemplates(type: Delete) {
-	// Delete the generated native libs
-	delete("lib/libs")
+    // Delete the generated native libs
+    delete("lib/libs")
+
+    // Delete the library generated AAR files
+    delete("lib/build/outputs/aar")
+
+    // Delete the godotpayment libs directory contents
+    delete("plugins/godotpayment/libs")
 
 
-	// Delete the library generated AAR files
-	delete("lib/build/outputs/aar")
+    // Delete the generated godotpayment aar
+    delete("plugins/godotpayment/build/outputs/aar")
 
 
-	// Delete the app libs directory contents
-	delete("app/libs")
+    // Delete the app libs directory contents
+    delete("app/libs")
 
 
-	// Delete the generated binary apks
-	delete("app/build/outputs/apk")
+    // Delete the generated binary apks
+    delete("app/build/outputs/apk")
 
 
-	// Delete the Godot templates in the Godot bin directory
-	delete("$binDir/android_debug.apk")
-	delete("$binDir/android_release.apk")
-	delete("$binDir/android_source.zip")
+    // Delete the Godot templates in the Godot bin directory
+    delete("$binDir/android_debug.apk")
+    delete("$binDir/android_release.apk")
+    delete("$binDir/android_source.zip")
+    delete("$binDir/godot-lib.debug.aar")
+    delete("$binDir/godot-lib.release.aar")
 }
 }

+ 1 - 0
platform/android/java/lib/build.gradle

@@ -2,6 +2,7 @@ apply plugin: 'com.android.library'
 
 
 dependencies {
 dependencies {
     implementation libraries.supportCoreUtils
     implementation libraries.supportCoreUtils
+    implementation libraries.v4Support
 }
 }
 
 
 def pathToRootDir = "../../../../"
 def pathToRootDir = "../../../../"

+ 68 - 6
platform/android/java/lib/src/org/godotengine/godot/Godot.java

@@ -61,8 +61,11 @@ import android.os.Messenger;
 import android.os.VibrationEffect;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.os.Vibrator;
 import android.provider.Settings.Secure;
 import android.provider.Settings.Secure;
+import android.support.annotation.CallSuper;
 import android.support.annotation.Keep;
 import android.support.annotation.Keep;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.Nullable;
+import android.support.v4.app.FragmentActivity;
 import android.view.Display;
 import android.view.Display;
 import android.view.KeyEvent;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.MotionEvent;
@@ -96,11 +99,13 @@ import java.util.Locale;
 import javax.microedition.khronos.opengles.GL10;
 import javax.microedition.khronos.opengles.GL10;
 import org.godotengine.godot.input.GodotEditText;
 import org.godotengine.godot.input.GodotEditText;
 import org.godotengine.godot.payments.PaymentsManager;
 import org.godotengine.godot.payments.PaymentsManager;
+import org.godotengine.godot.plugin.GodotPlugin;
+import org.godotengine.godot.plugin.GodotPluginRegistry;
 import org.godotengine.godot.utils.GodotNetUtils;
 import org.godotengine.godot.utils.GodotNetUtils;
 import org.godotengine.godot.utils.PermissionsUtil;
 import org.godotengine.godot.utils.PermissionsUtil;
 import org.godotengine.godot.xr.XRMode;
 import org.godotengine.godot.xr.XRMode;
 
 
-public abstract class Godot extends Activity implements SensorEventListener, IDownloaderClient {
+public abstract class Godot extends FragmentActivity implements SensorEventListener, IDownloaderClient {
 
 
 	static final int MAX_SINGLETONS = 64;
 	static final int MAX_SINGLETONS = 64;
 	private IStub mDownloaderClientStub;
 	private IStub mDownloaderClientStub;
@@ -129,6 +134,8 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo
 	// Used to dispatch events to the main thread.
 	// Used to dispatch events to the main thread.
 	private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
 	private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
 
 
+	private GodotPluginRegistry pluginRegistry;
+
 	static private Intent mCurrentIntent;
 	static private Intent mCurrentIntent;
 
 
 	@Override
 	@Override
@@ -158,7 +165,7 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo
 
 
 		protected void registerClass(String p_name, String[] p_methods) {
 		protected void registerClass(String p_name, String[] p_methods) {
 
 
-			GodotLib.singleton(p_name, this);
+			GodotPlugin.nativeRegisterSingleton(p_name, this);
 
 
 			Class clazz = getClass();
 			Class clazz = getClass();
 			Method[] methods = clazz.getDeclaredMethods();
 			Method[] methods = clazz.getDeclaredMethods();
@@ -184,7 +191,7 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo
 				String[] pt = new String[ptr.size()];
 				String[] pt = new String[ptr.size()];
 				ptr.toArray(pt);
 				ptr.toArray(pt);
 
 
-				GodotLib.method(p_name, method.getName(), method.getReturnType().getName(), pt);
+				GodotPlugin.nativeRegisterMethod(p_name, method.getName(), method.getReturnType().getName(), pt);
 			}
 			}
 
 
 			Godot.singletons[Godot.singleton_count++] = this;
 			Godot.singletons[Godot.singleton_count++] = this;
@@ -269,6 +276,9 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo
 
 
 			singletons[i].onMainActivityResult(requestCode, resultCode, data);
 			singletons[i].onMainActivityResult(requestCode, resultCode, data);
 		}
 		}
+		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
+			plugin.onMainActivityResult(requestCode, resultCode, data);
+		}
 	};
 	};
 
 
 	@Override
 	@Override
@@ -276,12 +286,25 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo
 		for (int i = 0; i < singleton_count; i++) {
 		for (int i = 0; i < singleton_count; i++) {
 			singletons[i].onMainRequestPermissionsResult(requestCode, permissions, grantResults);
 			singletons[i].onMainRequestPermissionsResult(requestCode, permissions, grantResults);
 		}
 		}
+		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
+			plugin.onMainRequestPermissionsResult(requestCode, permissions, grantResults);
+		}
 
 
 		for (int i = 0; i < permissions.length; i++) {
 		for (int i = 0; i < permissions.length; i++) {
 			GodotLib.requestPermissionResult(permissions[i], grantResults[i] == PackageManager.PERMISSION_GRANTED);
 			GodotLib.requestPermissionResult(permissions[i], grantResults[i] == PackageManager.PERMISSION_GRANTED);
 		}
 		}
 	};
 	};
 
 
+	/**
+	 * Invoked on the GL thread when the Godot main loop has started.
+	 */
+	@CallSuper
+	protected void onGLGodotMainLoopStarted() {
+		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
+			plugin.onGLGodotMainLoopStarted();
+		}
+	}
+
 	/**
 	/**
 	 * Used by the native code (java_godot_lib_jni.cpp) to complete initialization of the GLSurfaceView view and renderer.
 	 * Used by the native code (java_godot_lib_jni.cpp) to complete initialization of the GLSurfaceView view and renderer.
 	 */
 	 */
@@ -304,14 +327,13 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo
 		edittext.setView(mView);
 		edittext.setView(mView);
 		io.setEdit(edittext);
 		io.setEdit(edittext);
 
 
-		final Godot godot = this;
 		mView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
 		mView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
 			@Override
 			@Override
 			public void onGlobalLayout() {
 			public void onGlobalLayout() {
 				Point fullSize = new Point();
 				Point fullSize = new Point();
-				godot.getWindowManager().getDefaultDisplay().getSize(fullSize);
+				getWindowManager().getDefaultDisplay().getSize(fullSize);
 				Rect gameSize = new Rect();
 				Rect gameSize = new Rect();
-				godot.mView.getWindowVisibleDisplayFrame(gameSize);
+				mView.getWindowVisibleDisplayFrame(gameSize);
 
 
 				final int keyboardHeight = fullSize.y - gameSize.bottom;
 				final int keyboardHeight = fullSize.y - gameSize.bottom;
 				GodotLib.setVirtualKeyboardHeight(keyboardHeight);
 				GodotLib.setVirtualKeyboardHeight(keyboardHeight);
@@ -323,6 +345,12 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo
 			@Override
 			@Override
 			public void run() {
 			public void run() {
 				GodotLib.setup(current_command_line);
 				GodotLib.setup(current_command_line);
+
+				// Must occur after GodotLib.setup has completed.
+				for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
+					plugin.onGLRegisterPluginWithGodotNative();
+				}
+
 				setKeepScreenOn("True".equals(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on")));
 				setKeepScreenOn("True".equals(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on")));
 
 
 				// The Godot Android plugins are setup on completion of GodotLib.setup
 				// The Godot Android plugins are setup on completion of GodotLib.setup
@@ -340,6 +368,14 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo
 				});
 				});
 			}
 			}
 		});
 		});
+
+		// Include the returned non-null views in the Godot view hierarchy.
+		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
+			View pluginView = plugin.onMainCreateView(this);
+			if (pluginView != null) {
+				layout.addView(pluginView);
+			}
+		}
 	}
 	}
 
 
 	public void setKeepScreenOn(final boolean p_enabled) {
 	public void setKeepScreenOn(final boolean p_enabled) {
@@ -537,6 +573,7 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo
 		Window window = getWindow();
 		Window window = getWindow();
 		window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
 		window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
 		mClipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
 		mClipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
+		pluginRegistry = GodotPluginRegistry.initializePluginRegistry(this);
 
 
 		//check for apk expansion API
 		//check for apk expansion API
 		if (true) {
 		if (true) {
@@ -680,6 +717,9 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo
 		for (int i = 0; i < singleton_count; i++) {
 		for (int i = 0; i < singleton_count; i++) {
 			singletons[i].onMainDestroy();
 			singletons[i].onMainDestroy();
 		}
 		}
+		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
+			plugin.onMainDestroy();
+		}
 
 
 		GodotLib.ondestroy(this);
 		GodotLib.ondestroy(this);
 
 
@@ -708,6 +748,9 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo
 		for (int i = 0; i < singleton_count; i++) {
 		for (int i = 0; i < singleton_count; i++) {
 			singletons[i].onMainPause();
 			singletons[i].onMainPause();
 		}
 		}
+		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
+			plugin.onMainPause();
+		}
 	}
 	}
 
 
 	public String getClipboard() {
 	public String getClipboard() {
@@ -761,6 +804,9 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo
 
 
 			singletons[i].onMainResume();
 			singletons[i].onMainResume();
 		}
 		}
+		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
+			plugin.onMainResume();
+		}
 	}
 	}
 
 
 	public void UiChangeListener() {
 	public void UiChangeListener() {
@@ -857,6 +903,11 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo
 				shouldQuit = false;
 				shouldQuit = false;
 			}
 			}
 		}
 		}
+		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
+			if (plugin.onMainBackPressed()) {
+				shouldQuit = false;
+			}
+		}
 
 
 		if (shouldQuit && mView != null) {
 		if (shouldQuit && mView != null) {
 			mView.queueEvent(new Runnable() {
 			mView.queueEvent(new Runnable() {
@@ -868,6 +919,17 @@ public abstract class Godot extends Activity implements SensorEventListener, IDo
 		}
 		}
 	}
 	}
 
 
+	/**
+	 * Queue a runnable to be run on the GL thread.
+	 * <p>
+	 * This must be called after the GL thread has started.
+	 */
+	public final void runOnGLThread(@NonNull Runnable action) {
+		if (mView != null) {
+			mView.queueEvent(action);
+		}
+	}
+
 	private void forceQuit() {
 	private void forceQuit() {
 		System.exit(0);
 		System.exit(0);
 	}
 	}

+ 0 - 16
platform/android/java/lib/src/org/godotengine/godot/GodotLib.java

@@ -175,22 +175,6 @@ public class GodotLib {
 	 */
 	 */
 	public static native void audio();
 	public static native void audio();
 
 
-	/**
-	 * Used to setup a {@link org.godotengine.godot.Godot.SingletonBase} instance.
-	 * @param p_name Name of the instance.
-	 * @param p_object Reference to the singleton instance.
-	 */
-	public static native void singleton(String p_name, Object p_object);
-
-	/**
-	 * Used to complete registration of the {@link org.godotengine.godot.Godot.SingletonBase} instance's methods.
-	 * @param p_sname Name of the instance
-	 * @param p_name Name of the method to register
-	 * @param p_ret Return type of the registered method
-	 * @param p_params Method parameters types
-	 */
-	public static native void method(String p_sname, String p_name, String p_ret, String[] p_params);
-
 	/**
 	/**
 	 * Used to access Godot global properties.
 	 * Used to access Godot global properties.
 	 * @param p_key Property key
 	 * @param p_key Property key

+ 16 - 1
platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java

@@ -33,6 +33,8 @@ package org.godotengine.godot;
 import android.opengl.GLSurfaceView;
 import android.opengl.GLSurfaceView;
 import javax.microedition.khronos.egl.EGLConfig;
 import javax.microedition.khronos.egl.EGLConfig;
 import javax.microedition.khronos.opengles.GL10;
 import javax.microedition.khronos.opengles.GL10;
+import org.godotengine.godot.plugin.GodotPlugin;
+import org.godotengine.godot.plugin.GodotPluginRegistry;
 import org.godotengine.godot.utils.GLUtils;
 import org.godotengine.godot.utils.GLUtils;
 
 
 /**
 /**
@@ -40,8 +42,13 @@ import org.godotengine.godot.utils.GLUtils;
  */
  */
 class GodotRenderer implements GLSurfaceView.Renderer {
 class GodotRenderer implements GLSurfaceView.Renderer {
 
 
+	private final GodotPluginRegistry pluginRegistry;
 	private boolean activityJustResumed = false;
 	private boolean activityJustResumed = false;
 
 
+	GodotRenderer() {
+		this.pluginRegistry = GodotPluginRegistry.getPluginRegistry();
+	}
+
 	public void onDrawFrame(GL10 gl) {
 	public void onDrawFrame(GL10 gl) {
 		if (activityJustResumed) {
 		if (activityJustResumed) {
 			GodotLib.onRendererResumed();
 			GodotLib.onRendererResumed();
@@ -52,18 +59,26 @@ class GodotRenderer implements GLSurfaceView.Renderer {
 		for (int i = 0; i < Godot.singleton_count; i++) {
 		for (int i = 0; i < Godot.singleton_count; i++) {
 			Godot.singletons[i].onGLDrawFrame(gl);
 			Godot.singletons[i].onGLDrawFrame(gl);
 		}
 		}
+		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
+			plugin.onGLDrawFrame(gl);
+		}
 	}
 	}
 
 
 	public void onSurfaceChanged(GL10 gl, int width, int height) {
 	public void onSurfaceChanged(GL10 gl, int width, int height) {
-
 		GodotLib.resize(width, height);
 		GodotLib.resize(width, height);
 		for (int i = 0; i < Godot.singleton_count; i++) {
 		for (int i = 0; i < Godot.singleton_count; i++) {
 			Godot.singletons[i].onGLSurfaceChanged(gl, width, height);
 			Godot.singletons[i].onGLSurfaceChanged(gl, width, height);
 		}
 		}
+		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
+			plugin.onGLSurfaceChanged(gl, width, height);
+		}
 	}
 	}
 
 
 	public void onSurfaceCreated(GL10 gl, EGLConfig config) {
 	public void onSurfaceCreated(GL10 gl, EGLConfig config) {
 		GodotLib.newcontext(GLUtils.use_32);
 		GodotLib.newcontext(GLUtils.use_32);
+		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
+			plugin.onGLSurfaceCreated(gl, config);
+		}
 	}
 	}
 
 
 	void onActivityResumed() {
 	void onActivityResumed() {

+ 97 - 0
platform/android/java/lib/src/org/godotengine/godot/payments/GodotPaymentInterface.java

@@ -0,0 +1,97 @@
+/*************************************************************************/
+/*  GodotPaymentInterface.java                                           */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md)    */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+package org.godotengine.godot.payments;
+
+public interface GodotPaymentInterface {
+	void purchase(String sku, String transactionId);
+
+	void consumeUnconsumedPurchases();
+
+	String getSignature();
+
+	void callbackSuccess(String ticket, String signature, String sku);
+
+	void callbackSuccessProductMassConsumed(String ticket, String signature, String sku);
+
+	void callbackSuccessNoUnconsumedPurchases();
+
+	void callbackFailConsume(String message);
+
+	void callbackFail(String message);
+
+	void callbackCancel();
+
+	void callbackAlreadyOwned(String sku);
+
+	int getPurchaseCallbackId();
+
+	void setPurchaseCallbackId(int purchaseCallbackId);
+
+	String getPurchaseValidationUrlPrefix();
+
+	void setPurchaseValidationUrlPrefix(String url);
+
+	String getAccessToken();
+
+	void setAccessToken(String accessToken);
+
+	void setTransactionId(String transactionId);
+
+	String getTransactionId();
+
+	// request purchased items are not consumed
+	void requestPurchased();
+
+	// callback for requestPurchased()
+	void callbackPurchased(String receipt, String signature, String sku);
+
+	void callbackDisconnected();
+
+	void callbackConnected();
+
+	// true if connected, false otherwise
+	boolean isConnected();
+
+	// consume item automatically after purchase. default is true.
+	void setAutoConsume(boolean autoConsume);
+
+	// consume a specific item
+	void consume(String sku);
+
+	// query in app item detail info
+	void querySkuDetails(String[] list);
+
+	void addSkuDetail(String itemJson);
+
+	void completeSkuDetail();
+
+	void errorSkuDetail(String errorMessage);
+}

+ 33 - 34
platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java

@@ -43,7 +43,6 @@ import android.util.Log;
 import com.android.vending.billing.IInAppBillingService;
 import com.android.vending.billing.IInAppBillingService;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Arrays;
-import org.godotengine.godot.GodotPaymentV3;
 import org.json.JSONException;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.json.JSONObject;
 
 
@@ -90,9 +89,9 @@ public class PaymentsManager {
 		public void onServiceDisconnected(ComponentName name) {
 		public void onServiceDisconnected(ComponentName name) {
 			mService = null;
 			mService = null;
 
 
-			// At this stage, godotPaymentV3 might not have been initialized yet.
-			if (godotPaymentV3 != null) {
-				godotPaymentV3.callbackDisconnected();
+			// At this stage, godotPayment might not have been initialized yet.
+			if (godotPayment != null) {
+				godotPayment.callbackDisconnected();
 			}
 			}
 		}
 		}
 
 
@@ -100,9 +99,9 @@ public class PaymentsManager {
 		public void onServiceConnected(ComponentName name, IBinder service) {
 		public void onServiceConnected(ComponentName name, IBinder service) {
 			mService = IInAppBillingService.Stub.asInterface(service);
 			mService = IInAppBillingService.Stub.asInterface(service);
 
 
-			// At this stage, godotPaymentV3 might not have been initialized yet.
-			if (godotPaymentV3 != null) {
-				godotPaymentV3.callbackConnected();
+			// At this stage, godotPayment might not have been initialized yet.
+			if (godotPayment != null) {
+				godotPayment.callbackConnected();
 			}
 			}
 		}
 		}
 	};
 	};
@@ -111,17 +110,17 @@ public class PaymentsManager {
 		new PurchaseTask(mService, activity) {
 		new PurchaseTask(mService, activity) {
 			@Override
 			@Override
 			protected void error(String message) {
 			protected void error(String message) {
-				godotPaymentV3.callbackFail(message);
+				godotPayment.callbackFail(message);
 			}
 			}
 
 
 			@Override
 			@Override
 			protected void canceled() {
 			protected void canceled() {
-				godotPaymentV3.callbackCancel();
+				godotPayment.callbackCancel();
 			}
 			}
 
 
 			@Override
 			@Override
 			protected void alreadyOwned() {
 			protected void alreadyOwned() {
-				godotPaymentV3.callbackAlreadyOwned(sku);
+				godotPayment.callbackAlreadyOwned(sku);
 			}
 			}
 		}
 		}
 				.purchase(sku, transactionId);
 				.purchase(sku, transactionId);
@@ -135,19 +134,19 @@ public class PaymentsManager {
 		new ReleaseAllConsumablesTask(mService, activity) {
 		new ReleaseAllConsumablesTask(mService, activity) {
 			@Override
 			@Override
 			protected void success(String sku, String receipt, String signature, String token) {
 			protected void success(String sku, String receipt, String signature, String token) {
-				godotPaymentV3.callbackSuccessProductMassConsumed(receipt, signature, sku);
+				godotPayment.callbackSuccessProductMassConsumed(receipt, signature, sku);
 			}
 			}
 
 
 			@Override
 			@Override
 			protected void error(String message) {
 			protected void error(String message) {
 				Log.d("godot", "consumeUnconsumedPurchases :" + message);
 				Log.d("godot", "consumeUnconsumedPurchases :" + message);
-				godotPaymentV3.callbackFailConsume(message);
+				godotPayment.callbackFailConsume(message);
 			}
 			}
 
 
 			@Override
 			@Override
 			protected void notRequired() {
 			protected void notRequired() {
 				Log.d("godot", "callbackSuccessNoUnconsumedPurchases :");
 				Log.d("godot", "callbackSuccessNoUnconsumedPurchases :");
-				godotPaymentV3.callbackSuccessNoUnconsumedPurchases();
+				godotPayment.callbackSuccessNoUnconsumedPurchases();
 			}
 			}
 		}
 		}
 				.consumeItAll();
 				.consumeItAll();
@@ -168,7 +167,7 @@ public class PaymentsManager {
 					final ArrayList<String> mySignatures = bundle.getStringArrayList("INAPP_DATA_SIGNATURE_LIST");
 					final ArrayList<String> mySignatures = bundle.getStringArrayList("INAPP_DATA_SIGNATURE_LIST");
 
 
 					if (myPurchases == null || myPurchases.size() == 0) {
 					if (myPurchases == null || myPurchases.size() == 0) {
-						godotPaymentV3.callbackPurchased("", "", "");
+						godotPayment.callbackPurchased("", "", "");
 						return;
 						return;
 					}
 					}
 
 
@@ -186,7 +185,7 @@ public class PaymentsManager {
 							pc.setConsumableFlag("block", sku, true);
 							pc.setConsumableFlag("block", sku, true);
 							pc.setConsumableValue("token", sku, token);
 							pc.setConsumableValue("token", sku, token);
 
 
-							godotPaymentV3.callbackPurchased(receipt, signature, sku);
+							godotPayment.callbackPurchased(receipt, signature, sku);
 						} catch (JSONException e) {
 						} catch (JSONException e) {
 						}
 						}
 					}
 					}
@@ -203,7 +202,7 @@ public class PaymentsManager {
 		new HandlePurchaseTask(activity) {
 		new HandlePurchaseTask(activity) {
 			@Override
 			@Override
 			protected void success(final String sku, final String signature, final String ticket) {
 			protected void success(final String sku, final String signature, final String ticket) {
-				godotPaymentV3.callbackSuccess(ticket, signature, sku);
+				godotPayment.callbackSuccess(ticket, signature, sku);
 
 
 				if (auto_consume) {
 				if (auto_consume) {
 					new ConsumeTask(mService, activity) {
 					new ConsumeTask(mService, activity) {
@@ -213,7 +212,7 @@ public class PaymentsManager {
 
 
 						@Override
 						@Override
 						protected void error(String message) {
 						protected void error(String message) {
-							godotPaymentV3.callbackFail(message);
+							godotPayment.callbackFail(message);
 						}
 						}
 					}
 					}
 							.consume(sku);
 							.consume(sku);
@@ -222,12 +221,12 @@ public class PaymentsManager {
 
 
 			@Override
 			@Override
 			protected void error(String message) {
 			protected void error(String message) {
-				godotPaymentV3.callbackFail(message);
+				godotPayment.callbackFail(message);
 			}
 			}
 
 
 			@Override
 			@Override
 			protected void canceled() {
 			protected void canceled() {
-				godotPaymentV3.callbackCancel();
+				godotPayment.callbackCancel();
 			}
 			}
 		}
 		}
 				.handlePurchaseRequest(resultCode, data);
 				.handlePurchaseRequest(resultCode, data);
@@ -235,19 +234,19 @@ public class PaymentsManager {
 
 
 	public void validatePurchase(String purchaseToken, final String sku) {
 	public void validatePurchase(String purchaseToken, final String sku) {
 
 
-		new ValidateTask(activity, godotPaymentV3) {
+		new ValidateTask(activity, godotPayment) {
 			@Override
 			@Override
 			protected void success() {
 			protected void success() {
 
 
 				new ConsumeTask(mService, activity) {
 				new ConsumeTask(mService, activity) {
 					@Override
 					@Override
 					protected void success(String ticket) {
 					protected void success(String ticket) {
-						godotPaymentV3.callbackSuccess(ticket, null, sku);
+						godotPayment.callbackSuccess(ticket, null, sku);
 					}
 					}
 
 
 					@Override
 					@Override
 					protected void error(String message) {
 					protected void error(String message) {
-						godotPaymentV3.callbackFail(message);
+						godotPayment.callbackFail(message);
 					}
 					}
 				}
 				}
 						.consume(sku);
 						.consume(sku);
@@ -255,12 +254,12 @@ public class PaymentsManager {
 
 
 			@Override
 			@Override
 			protected void error(String message) {
 			protected void error(String message) {
-				godotPaymentV3.callbackFail(message);
+				godotPayment.callbackFail(message);
 			}
 			}
 
 
 			@Override
 			@Override
 			protected void canceled() {
 			protected void canceled() {
-				godotPaymentV3.callbackCancel();
+				godotPayment.callbackCancel();
 			}
 			}
 		}
 		}
 				.validatePurchase(sku);
 				.validatePurchase(sku);
@@ -274,12 +273,12 @@ public class PaymentsManager {
 		new ConsumeTask(mService, activity) {
 		new ConsumeTask(mService, activity) {
 			@Override
 			@Override
 			protected void success(String ticket) {
 			protected void success(String ticket) {
-				godotPaymentV3.callbackSuccessProductMassConsumed(ticket, "", sku);
+				godotPayment.callbackSuccessProductMassConsumed(ticket, "", sku);
 			}
 			}
 
 
 			@Override
 			@Override
 			protected void error(String message) {
 			protected void error(String message) {
-				godotPaymentV3.callbackFailConsume(message);
+				godotPayment.callbackFailConsume(message);
 			}
 			}
 		}
 		}
 				.consume(sku);
 				.consume(sku);
@@ -387,9 +386,9 @@ public class PaymentsManager {
 						if (!skuDetails.containsKey("DETAILS_LIST")) {
 						if (!skuDetails.containsKey("DETAILS_LIST")) {
 							int response = getResponseCodeFromBundle(skuDetails);
 							int response = getResponseCodeFromBundle(skuDetails);
 							if (response != BILLING_RESPONSE_RESULT_OK) {
 							if (response != BILLING_RESPONSE_RESULT_OK) {
-								godotPaymentV3.errorSkuDetail(getResponseDesc(response));
+								godotPayment.errorSkuDetail(getResponseDesc(response));
 							} else {
 							} else {
-								godotPaymentV3.errorSkuDetail("No error but no detail list.");
+								godotPayment.errorSkuDetail("No error but no detail list.");
 							}
 							}
 							return;
 							return;
 						}
 						}
@@ -398,22 +397,22 @@ public class PaymentsManager {
 
 
 						for (String thisResponse : responseList) {
 						for (String thisResponse : responseList) {
 							Log.d("godot", "response = " + thisResponse);
 							Log.d("godot", "response = " + thisResponse);
-							godotPaymentV3.addSkuDetail(thisResponse);
+							godotPayment.addSkuDetail(thisResponse);
 						}
 						}
 					} catch (RemoteException e) {
 					} catch (RemoteException e) {
 						e.printStackTrace();
 						e.printStackTrace();
-						godotPaymentV3.errorSkuDetail("RemoteException error!");
+						godotPayment.errorSkuDetail("RemoteException error!");
 					}
 					}
 				}
 				}
-				godotPaymentV3.completeSkuDetail();
+				godotPayment.completeSkuDetail();
 			}
 			}
 		}))
 		}))
 				.start();
 				.start();
 	}
 	}
 
 
-	private GodotPaymentV3 godotPaymentV3;
+	private GodotPaymentInterface godotPayment;
 
 
-	public void setBaseSingleton(GodotPaymentV3 godotPaymentV3) {
-		this.godotPaymentV3 = godotPaymentV3;
+	public void setBaseSingleton(GodotPaymentInterface godotPaymentInterface) {
+		this.godotPayment = godotPaymentInterface;
 	}
 	}
 }
 }

+ 4 - 5
platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java

@@ -34,7 +34,6 @@ import android.app.Activity;
 import android.app.ProgressDialog;
 import android.app.ProgressDialog;
 import android.os.AsyncTask;
 import android.os.AsyncTask;
 import java.lang.ref.WeakReference;
 import java.lang.ref.WeakReference;
-import org.godotengine.godot.GodotPaymentV3;
 import org.godotengine.godot.utils.HttpRequester;
 import org.godotengine.godot.utils.HttpRequester;
 import org.godotengine.godot.utils.RequestParams;
 import org.godotengine.godot.utils.RequestParams;
 import org.json.JSONException;
 import org.json.JSONException;
@@ -43,7 +42,7 @@ import org.json.JSONObject;
 abstract public class ValidateTask {
 abstract public class ValidateTask {
 
 
 	private Activity context;
 	private Activity context;
-	private GodotPaymentV3 godotPaymentsV3;
+	private GodotPaymentInterface godotPayments;
 	private ProgressDialog dialog;
 	private ProgressDialog dialog;
 	private String mSku;
 	private String mSku;
 
 
@@ -80,9 +79,9 @@ abstract public class ValidateTask {
 		}
 		}
 	}
 	}
 
 
-	public ValidateTask(Activity context, GodotPaymentV3 godotPaymentsV3) {
+	public ValidateTask(Activity context, GodotPaymentInterface godotPayments) {
 		this.context = context;
 		this.context = context;
-		this.godotPaymentsV3 = godotPaymentsV3;
+		this.godotPayments = godotPayments;
 	}
 	}
 
 
 	public void validatePurchase(final String sku) {
 	public void validatePurchase(final String sku) {
@@ -96,7 +95,7 @@ abstract public class ValidateTask {
 
 
 	private String doInBackground(String... params) {
 	private String doInBackground(String... params) {
 		PaymentsCache pc = new PaymentsCache(context);
 		PaymentsCache pc = new PaymentsCache(context);
-		String url = godotPaymentsV3.getPurchaseValidationUrlPrefix();
+		String url = godotPayments.getPurchaseValidationUrlPrefix();
 		RequestParams param = new RequestParams();
 		RequestParams param = new RequestParams();
 		param.setUrl(url);
 		param.setUrl(url);
 		param.put("ticket", pc.getConsumableValue("ticket", mSku));
 		param.put("ticket", pc.getConsumableValue("ticket", mSku));

+ 256 - 0
platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java

@@ -0,0 +1,256 @@
+/*************************************************************************/
+/*  GodotPlugin.java                                                     */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md)    */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+package org.godotengine.godot.plugin;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.View;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+import org.godotengine.godot.Godot;
+
+/**
+ * Base class for the Godot Android plugins.
+ * <p>
+ * A Godot Android plugin is a regular Android library packaged as an aar archive file with the following caveats:
+ * <p>
+ * - The library must have a dependency on the Godot Android library (godot-lib.aar).
+ * A stable version is available for each release.
+ * <p>
+ * - The library must include a <meta-data> tag in its manifest file setup as follow:
+ * <meta-data android:name="org.godotengine.plugin.v1.[PluginName]" android:value="[plugin.init.ClassFullName]" />
+ * Where:
+ * - 'PluginName' is the name of the plugin.
+ * - 'plugin.init.ClassFullName' is the full name (package + class name) of the plugin class
+ * extending {@link GodotPlugin}.
+ *
+ * A plugin can also define and provide c/c++ gdnative libraries and nativescripts for the target
+ * app/game to leverage.
+ * The shared library for the gdnative library will be automatically bundled by the aar build
+ * system.
+ * Godot '*.gdnlib' and '*.gdns' resource files must however be manually defined in the project
+ * 'assets' directory. The recommended path for these resources in the 'assets' directory should be:
+ * 'godot/plugin/v1/[PluginName]/'
+ */
+public abstract class GodotPlugin {
+
+	private final Godot godot;
+
+	public GodotPlugin(Godot godot) {
+		this.godot = godot;
+	}
+
+	/**
+	 * Provides access to the Godot engine.
+	 */
+	protected Godot getGodot() {
+		return godot;
+	}
+
+	/**
+	 * Register the plugin with Godot native code.
+	 */
+	public final void onGLRegisterPluginWithGodotNative() {
+		nativeRegisterSingleton(getPluginName(), this);
+
+		Class clazz = getClass();
+		Method[] methods = clazz.getDeclaredMethods();
+		for (Method method : methods) {
+			boolean found = false;
+
+			for (String s : getPluginMethods()) {
+				if (s.equals(method.getName())) {
+					found = true;
+					break;
+				}
+			}
+			if (!found)
+				continue;
+
+			List<String> ptr = new ArrayList<String>();
+
+			Class[] paramTypes = method.getParameterTypes();
+			for (Class c : paramTypes) {
+				ptr.add(c.getName());
+			}
+
+			String[] pt = new String[ptr.size()];
+			ptr.toArray(pt);
+
+			nativeRegisterMethod(getPluginName(), method.getName(), method.getReturnType().getName(), pt);
+		}
+
+		// Get the list of gdnative libraries to register.
+		Set<String> gdnativeLibrariesPaths = getPluginGDNativeLibrariesPaths();
+		if (!gdnativeLibrariesPaths.isEmpty()) {
+			nativeRegisterGDNativeLibraries(gdnativeLibrariesPaths.toArray(new String[0]));
+		}
+	}
+
+	/**
+	 * 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.
+	 *
+	 * @return the view to be included; null if no views should be included.
+	 */
+	@Nullable
+	public View onMainCreateView(Activity activity) {
+		return null;
+	}
+
+	/**
+	 * @see Activity#onActivityResult(int, int, Intent)
+	 */
+	public void onMainActivityResult(int requestCode, int resultCode, Intent data) {
+	}
+
+	/**
+	 * @see Activity#onRequestPermissionsResult(int, String[], int[])
+	 */
+	public void onMainRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+	}
+
+	/**
+	 * @see Activity#onPause()
+	 */
+	public void onMainPause() {}
+
+	/**
+	 * @see Activity#onResume()
+	 */
+	public void onMainResume() {}
+
+	/**
+	 * @see Activity#onDestroy()
+	 */
+	public void onMainDestroy() {}
+
+	/**
+	 * @see Activity#onBackPressed()
+	 */
+	public boolean onMainBackPressed() { return false; }
+
+	/**
+	 * Invoked on the GL thread when the Godot main loop has started.
+	 */
+	public void onGLGodotMainLoopStarted() {}
+
+	/**
+	 * Invoked once per frame on the GL thread after the frame is drawn.
+	 */
+	public void onGLDrawFrame(GL10 gl) {}
+
+	/**
+	 * Called on the GL thread after the surface is created and whenever the OpenGL ES surface size
+	 * changes.
+	 */
+	public void onGLSurfaceChanged(GL10 gl, int width, int height) {}
+
+	/**
+	 * Called on the GL thread when the surface is created or recreated.
+	 */
+	public void onGLSurfaceCreated(GL10 gl, EGLConfig config) {}
+
+	/**
+	 * Returns the name of the plugin.
+	 * <p>
+	 * This value must match the one listed in the plugin '<meta-data>' manifest entry.
+	 */
+	@NonNull
+	public abstract String getPluginName();
+
+	/**
+	 * Returns the list of methods to be exposed to Godot.
+	 */
+	@NonNull
+	public abstract List<String> getPluginMethods();
+
+	/**
+	 * Returns the paths for the plugin's gdnative libraries.
+	 *
+	 * The paths must be relative to the 'assets' directory and point to a '*.gdnlib' file.
+	 */
+	@NonNull
+	protected Set<String> getPluginGDNativeLibrariesPaths() {
+		return Collections.emptySet();
+	}
+
+	/**
+	 * Runs the specified action on the UI thread. If the current thread is the UI
+	 * thread, then the action is executed immediately. If the current thread is
+	 * not the UI thread, the action is posted to the event queue of the UI thread.
+	 *
+	 * @param action the action to run on the UI thread
+	 */
+	protected void runOnUiThread(Runnable action) {
+		godot.runOnUiThread(action);
+	}
+
+	/**
+	 * Queue the specified action to be run on the GL thread.
+	 *
+	 * @param action the action to run on the GL thread
+	 */
+	protected void runOnGLThread(Runnable action) {
+		godot.runOnGLThread(action);
+	}
+
+	/**
+	 * Used to setup a {@link GodotPlugin} instance.
+	 * @param p_name Name of the instance.
+	 */
+	public static native void nativeRegisterSingleton(String p_name, Object object);
+
+	/**
+	 * Used to complete registration of the {@link GodotPlugin} instance's methods.
+	 * @param p_sname Name of the instance
+	 * @param p_name Name of the method to register
+	 * @param p_ret Return type of the registered method
+	 * @param p_params Method parameters types
+	 */
+	public static native void nativeRegisterMethod(String p_sname, String p_name, String p_ret, String[] p_params);
+
+	/**
+	 * Used to register gdnative libraries bundled by the plugin.
+	 * @param gdnlibPaths Paths to the libraries relative to the 'assets' directory.
+	 */
+	private native void nativeRegisterGDNativeLibraries(String[] gdnlibPaths);
+}

+ 199 - 0
platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java

@@ -0,0 +1,199 @@
+/*************************************************************************/
+/*  GodotPluginRegistry.java                                             */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md)    */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+package org.godotengine.godot.plugin;
+
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import org.godotengine.godot.Godot;
+
+/**
+ * Registry used to load and access the registered Godot Android plugins.
+ */
+public final class GodotPluginRegistry {
+
+	private static final String TAG = GodotPluginRegistry.class.getSimpleName();
+
+	private static final String GODOT_PLUGIN_V1_NAME_PREFIX = "org.godotengine.plugin.v1.";
+
+	/**
+	 * Name for the metadata containing the list of Godot plugins to enable.
+	 */
+	private static final String GODOT_ENABLED_PLUGINS_LABEL = "custom_template_plugins";
+
+	private static GodotPluginRegistry instance;
+	private final ConcurrentHashMap<String, GodotPlugin> registry;
+
+	private GodotPluginRegistry(Godot godot) {
+		registry = new ConcurrentHashMap<>();
+		loadPlugins(godot);
+	}
+
+	/**
+	 * Retrieve the plugin tied to the given plugin name.
+	 * @param pluginName Name of the plugin
+	 * @return {@link GodotPlugin} handle if it exists, null otherwise.
+	 */
+	@Nullable
+	public GodotPlugin getPlugin(String pluginName) {
+		return registry.get(pluginName);
+	}
+
+	/**
+	 * Retrieve the full set of loaded plugins.
+	 */
+	public Collection<GodotPlugin> getAllPlugins() {
+		return registry.values();
+	}
+
+	/**
+	 * Parse the manifest file and load all included Godot Android plugins.
+	 * <p>
+	 * A plugin manifest entry is a '<meta-data>' tag setup as described in the {@link GodotPlugin}
+	 * documentation.
+	 *
+	 * @param godot Godot instance
+	 * @return A singleton instance of {@link GodotPluginRegistry}. This ensures that only one instance
+	 * of each Godot Android plugins is available at runtime.
+	 */
+	public static GodotPluginRegistry initializePluginRegistry(Godot godot) {
+		if (instance == null) {
+			instance = new GodotPluginRegistry(godot);
+		}
+
+		return instance;
+	}
+
+	/**
+	 * Return the plugin registry if it's initialized.
+	 * Throws a {@link IllegalStateException} exception if not.
+	 *
+	 * @throws IllegalStateException if {@link GodotPluginRegistry#initializePluginRegistry(Godot)} has not been called prior to calling this method.
+	 */
+	public static GodotPluginRegistry getPluginRegistry() throws IllegalStateException {
+		if (instance == null) {
+			throw new IllegalStateException("Plugin registry hasn't been initialized.");
+		}
+
+		return instance;
+	}
+
+	private void loadPlugins(Godot godot) {
+		try {
+			ApplicationInfo appInfo = godot
+											  .getPackageManager()
+											  .getApplicationInfo(godot.getPackageName(), PackageManager.GET_META_DATA);
+			Bundle metaData = appInfo.metaData;
+			if (metaData == null || metaData.isEmpty()) {
+				return;
+			}
+
+			// When using the Godot editor for building and exporting the apk, this is used to check
+			// which plugins to enable since the custom build template may contain prebuilt plugins.
+			// When using a custom process to generate the apk, the metadata is not needed since
+			// it's assumed that the developer is aware of the dependencies included in the apk.
+			final Set<String> enabledPluginsSet;
+			if (metaData.containsKey(GODOT_ENABLED_PLUGINS_LABEL)) {
+				String enabledPlugins = metaData.getString(GODOT_ENABLED_PLUGINS_LABEL, "");
+				String[] enabledPluginsList = enabledPlugins.split(",");
+				if (enabledPluginsList.length == 0) {
+					// No plugins to enable. Aborting early.
+					return;
+				}
+
+				enabledPluginsSet = new HashSet<>();
+				for (String enabledPlugin : enabledPluginsList) {
+					enabledPluginsSet.add(enabledPlugin.trim());
+				}
+			} else {
+				enabledPluginsSet = null;
+			}
+
+			int godotPluginV1NamePrefixLength = GODOT_PLUGIN_V1_NAME_PREFIX.length();
+			for (String metaDataName : metaData.keySet()) {
+				// Parse the meta-data looking for entry with the Godot plugin name prefix.
+				if (metaDataName.startsWith(GODOT_PLUGIN_V1_NAME_PREFIX)) {
+					String pluginName = metaDataName.substring(godotPluginV1NamePrefixLength).trim();
+					if (enabledPluginsSet != null && !enabledPluginsSet.contains(pluginName)) {
+						Log.w(TAG, "Plugin " + pluginName + " is listed in the dependencies but is not enabled.");
+						continue;
+					}
+
+					// Retrieve the plugin class full name.
+					String pluginHandleClassFullName = metaData.getString(metaDataName);
+					if (!TextUtils.isEmpty(pluginHandleClassFullName)) {
+						try {
+							// Attempt to create the plugin init class via reflection.
+							@SuppressWarnings("unchecked")
+							Class<GodotPlugin> pluginClass = (Class<GodotPlugin>)Class
+																	 .forName(pluginHandleClassFullName);
+							Constructor<GodotPlugin> pluginConstructor = pluginClass
+																				 .getConstructor(Godot.class);
+							GodotPlugin pluginHandle = pluginConstructor.newInstance(godot);
+
+							// Load the plugin initializer into the registry using the plugin name
+							// as key.
+							if (!pluginName.equals(pluginHandle.getPluginName())) {
+								Log.w(TAG,
+										"Meta-data plugin name does not match the value returned by the plugin handle: " + pluginName + " =/= " + pluginHandle.getPluginName());
+							}
+							registry.put(pluginName, pluginHandle);
+						} catch (ClassNotFoundException e) {
+							Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
+						} catch (IllegalAccessException e) {
+							Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
+						} catch (InstantiationException e) {
+							Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
+						} catch (NoSuchMethodException e) {
+							Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
+						} catch (InvocationTargetException e) {
+							Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
+						}
+					} else {
+						Log.w(TAG, "Invalid plugin loader class for " + pluginName);
+					}
+				}
+			}
+		} catch (PackageManager.NameNotFoundException e) {
+			Log.e(TAG, "Unable load Godot Android plugins from the manifest file.", e);
+		}
+	}
+}

+ 31 - 0
platform/android/java/plugins/godotpayment/build.gradle

@@ -0,0 +1,31 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion versions.compileSdk
+    buildToolsVersion versions.buildTools
+
+    defaultConfig {
+        minSdkVersion versions.minSdk
+        targetSdkVersion versions.targetSdk
+    }
+
+    libraryVariants.all { variant ->
+        variant.outputs.all { output ->
+            output.outputFileName = "GodotPayment.${variant.name}.aar"
+        }
+    }
+
+}
+
+dependencies {
+    implementation libraries.supportCoreUtils
+    implementation libraries.v4Support
+
+    if (rootProject.findProject(":lib")) {
+        compileOnly project(":lib")
+    } else if (rootProject.findProject(":godot:lib")) {
+        compileOnly project(":godot:lib")
+    } else {
+        compileOnly fileTree(dir: 'libs', include: ['godot-lib*.aar'])
+    }
+}

+ 11 - 0
platform/android/java/plugins/godotpayment/src/main/AndroidManifest.xml

@@ -0,0 +1,11 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="org.godotengine.godot.plugin.payment">
+
+    <application>
+
+        <meta-data
+            android:name="org.godotengine.plugin.v1.GodotPayment"
+            android:value="org.godotengine.godot.plugin.payment.GodotPayment" />
+
+    </application>
+</manifest>

+ 59 - 21
platform/android/java/lib/src/org/godotengine/godot/GodotPaymentV3.java → platform/android/java/plugins/godotpayment/src/main/java/org/godotengine/godot/plugin/payment/GodotPayment.java

@@ -1,5 +1,5 @@
 /*************************************************************************/
 /*************************************************************************/
-/*  GodotPaymentV3.java                                                  */
+/*  GodotPayment.java                                                    */
 /*************************************************************************/
 /*************************************************************************/
 /*                       This file is part of:                           */
 /*                       This file is part of:                           */
 /*                           GODOT ENGINE                                */
 /*                           GODOT ENGINE                                */
@@ -28,20 +28,24 @@
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
 /*************************************************************************/
 /*************************************************************************/
 
 
-package org.godotengine.godot;
+package org.godotengine.godot.plugin.payment;
 
 
-import android.app.Activity;
+import android.support.annotation.NonNull;
 import android.util.Log;
 import android.util.Log;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Arrays;
 import java.util.List;
 import java.util.List;
+import org.godotengine.godot.Dictionary;
+import org.godotengine.godot.Godot;
+import org.godotengine.godot.GodotLib;
+import org.godotengine.godot.payments.GodotPaymentInterface;
 import org.godotengine.godot.payments.PaymentsManager;
 import org.godotengine.godot.payments.PaymentsManager;
+import org.godotengine.godot.plugin.GodotPlugin;
 import org.json.JSONException;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.json.JSONObject;
 
 
-public class GodotPaymentV3 extends Godot.SingletonBase {
+public class GodotPayment extends GodotPlugin implements GodotPaymentInterface {
 
 
-	private Godot activity;
 	private Integer purchaseCallbackId = 0;
 	private Integer purchaseCallbackId = 0;
 	private String accessToken;
 	private String accessToken;
 	private String purchaseValidationUrlPrefix;
 	private String purchaseValidationUrlPrefix;
@@ -49,8 +53,15 @@ public class GodotPaymentV3 extends Godot.SingletonBase {
 	private PaymentsManager mPaymentManager;
 	private PaymentsManager mPaymentManager;
 	private Dictionary mSkuDetails = new Dictionary();
 	private Dictionary mSkuDetails = new Dictionary();
 
 
+	public GodotPayment(Godot godot) {
+		super(godot);
+		mPaymentManager = godot.getPaymentsManager();
+		mPaymentManager.setBaseSingleton(this);
+	}
+
+	@Override
 	public void purchase(final String sku, final String transactionId) {
 	public void purchase(final String sku, final String transactionId) {
-		activity.runOnUiThread(new Runnable() {
+		runOnUiThread(new Runnable() {
 			@Override
 			@Override
 			public void run() {
 			public void run() {
 				mPaymentManager.requestPurchase(sku, transactionId);
 				mPaymentManager.requestPurchase(sku, transactionId);
@@ -58,21 +69,9 @@ public class GodotPaymentV3 extends Godot.SingletonBase {
 		});
 		});
 	}
 	}
 
 
-	static public Godot.SingletonBase initialize(Activity p_activity) {
-
-		return new GodotPaymentV3(p_activity);
-	}
-
-	public GodotPaymentV3(Activity p_activity) {
-
-		registerClass("GodotPayments", new String[] { "purchase", "setPurchaseCallbackId", "setPurchaseValidationUrlPrefix", "setTransactionId", "getSignature", "consumeUnconsumedPurchases", "requestPurchased", "setAutoConsume", "consume", "querySkuDetails", "isConnected" });
-		activity = (Godot)p_activity;
-		mPaymentManager = activity.getPaymentsManager();
-		mPaymentManager.setBaseSingleton(this);
-	}
-
+	@Override
 	public void consumeUnconsumedPurchases() {
 	public void consumeUnconsumedPurchases() {
-		activity.runOnUiThread(new Runnable() {
+		runOnUiThread(new Runnable() {
 			@Override
 			@Override
 			public void run() {
 			public void run() {
 				mPaymentManager.consumeUnconsumedPurchases();
 				mPaymentManager.consumeUnconsumedPurchases();
@@ -82,74 +81,91 @@ public class GodotPaymentV3 extends Godot.SingletonBase {
 
 
 	private String signature;
 	private String signature;
 
 
+	@Override
 	public String getSignature() {
 	public String getSignature() {
 		return this.signature;
 		return this.signature;
 	}
 	}
 
 
+	@Override
 	public void callbackSuccess(String ticket, String signature, String sku) {
 	public void callbackSuccess(String ticket, String signature, String sku) {
 		GodotLib.calldeferred(purchaseCallbackId, "purchase_success", new Object[] { ticket, signature, sku });
 		GodotLib.calldeferred(purchaseCallbackId, "purchase_success", new Object[] { ticket, signature, sku });
 	}
 	}
 
 
+	@Override
 	public void callbackSuccessProductMassConsumed(String ticket, String signature, String sku) {
 	public void callbackSuccessProductMassConsumed(String ticket, String signature, String sku) {
 		Log.d(this.getClass().getName(), "callbackSuccessProductMassConsumed > " + ticket + "," + signature + "," + sku);
 		Log.d(this.getClass().getName(), "callbackSuccessProductMassConsumed > " + ticket + "," + signature + "," + sku);
 		GodotLib.calldeferred(purchaseCallbackId, "consume_success", new Object[] { ticket, signature, sku });
 		GodotLib.calldeferred(purchaseCallbackId, "consume_success", new Object[] { ticket, signature, sku });
 	}
 	}
 
 
+	@Override
 	public void callbackSuccessNoUnconsumedPurchases() {
 	public void callbackSuccessNoUnconsumedPurchases() {
 		GodotLib.calldeferred(purchaseCallbackId, "consume_not_required", new Object[] {});
 		GodotLib.calldeferred(purchaseCallbackId, "consume_not_required", new Object[] {});
 	}
 	}
 
 
+	@Override
 	public void callbackFailConsume(String message) {
 	public void callbackFailConsume(String message) {
 		GodotLib.calldeferred(purchaseCallbackId, "consume_fail", new Object[] { message });
 		GodotLib.calldeferred(purchaseCallbackId, "consume_fail", new Object[] { message });
 	}
 	}
 
 
+	@Override
 	public void callbackFail(String message) {
 	public void callbackFail(String message) {
 		GodotLib.calldeferred(purchaseCallbackId, "purchase_fail", new Object[] { message });
 		GodotLib.calldeferred(purchaseCallbackId, "purchase_fail", new Object[] { message });
 	}
 	}
 
 
+	@Override
 	public void callbackCancel() {
 	public void callbackCancel() {
 		GodotLib.calldeferred(purchaseCallbackId, "purchase_cancel", new Object[] {});
 		GodotLib.calldeferred(purchaseCallbackId, "purchase_cancel", new Object[] {});
 	}
 	}
 
 
+	@Override
 	public void callbackAlreadyOwned(String sku) {
 	public void callbackAlreadyOwned(String sku) {
 		GodotLib.calldeferred(purchaseCallbackId, "purchase_owned", new Object[] { sku });
 		GodotLib.calldeferred(purchaseCallbackId, "purchase_owned", new Object[] { sku });
 	}
 	}
 
 
+	@Override
 	public int getPurchaseCallbackId() {
 	public int getPurchaseCallbackId() {
 		return purchaseCallbackId;
 		return purchaseCallbackId;
 	}
 	}
 
 
+	@Override
 	public void setPurchaseCallbackId(int purchaseCallbackId) {
 	public void setPurchaseCallbackId(int purchaseCallbackId) {
 		this.purchaseCallbackId = purchaseCallbackId;
 		this.purchaseCallbackId = purchaseCallbackId;
 	}
 	}
 
 
+	@Override
 	public String getPurchaseValidationUrlPrefix() {
 	public String getPurchaseValidationUrlPrefix() {
 		return this.purchaseValidationUrlPrefix;
 		return this.purchaseValidationUrlPrefix;
 	}
 	}
 
 
+	@Override
 	public void setPurchaseValidationUrlPrefix(String url) {
 	public void setPurchaseValidationUrlPrefix(String url) {
 		this.purchaseValidationUrlPrefix = url;
 		this.purchaseValidationUrlPrefix = url;
 	}
 	}
 
 
+	@Override
 	public String getAccessToken() {
 	public String getAccessToken() {
 		return accessToken;
 		return accessToken;
 	}
 	}
 
 
+	@Override
 	public void setAccessToken(String accessToken) {
 	public void setAccessToken(String accessToken) {
 		this.accessToken = accessToken;
 		this.accessToken = accessToken;
 	}
 	}
 
 
+	@Override
 	public void setTransactionId(String transactionId) {
 	public void setTransactionId(String transactionId) {
 		this.transactionId = transactionId;
 		this.transactionId = transactionId;
 	}
 	}
 
 
+	@Override
 	public String getTransactionId() {
 	public String getTransactionId() {
 		return this.transactionId;
 		return this.transactionId;
 	}
 	}
 
 
 	// request purchased items are not consumed
 	// request purchased items are not consumed
+	@Override
 	public void requestPurchased() {
 	public void requestPurchased() {
-		activity.runOnUiThread(new Runnable() {
+		runOnUiThread(new Runnable() {
 			@Override
 			@Override
 			public void run() {
 			public void run() {
 				mPaymentManager.requestPurchased();
 				mPaymentManager.requestPurchased();
@@ -158,34 +174,41 @@ public class GodotPaymentV3 extends Godot.SingletonBase {
 	}
 	}
 
 
 	// callback for requestPurchased()
 	// callback for requestPurchased()
+	@Override
 	public void callbackPurchased(String receipt, String signature, String sku) {
 	public void callbackPurchased(String receipt, String signature, String sku) {
 		GodotLib.calldeferred(purchaseCallbackId, "has_purchased", new Object[] { receipt, signature, sku });
 		GodotLib.calldeferred(purchaseCallbackId, "has_purchased", new Object[] { receipt, signature, sku });
 	}
 	}
 
 
+	@Override
 	public void callbackDisconnected() {
 	public void callbackDisconnected() {
 		GodotLib.calldeferred(purchaseCallbackId, "iap_disconnected", new Object[] {});
 		GodotLib.calldeferred(purchaseCallbackId, "iap_disconnected", new Object[] {});
 	}
 	}
 
 
+	@Override
 	public void callbackConnected() {
 	public void callbackConnected() {
 		GodotLib.calldeferred(purchaseCallbackId, "iap_connected", new Object[] {});
 		GodotLib.calldeferred(purchaseCallbackId, "iap_connected", new Object[] {});
 	}
 	}
 
 
 	// true if connected, false otherwise
 	// true if connected, false otherwise
+	@Override
 	public boolean isConnected() {
 	public boolean isConnected() {
 		return mPaymentManager.isConnected();
 		return mPaymentManager.isConnected();
 	}
 	}
 
 
 	// consume item automatically after purchase. default is true.
 	// consume item automatically after purchase. default is true.
+	@Override
 	public void setAutoConsume(boolean autoConsume) {
 	public void setAutoConsume(boolean autoConsume) {
 		mPaymentManager.setAutoConsume(autoConsume);
 		mPaymentManager.setAutoConsume(autoConsume);
 	}
 	}
 
 
 	// consume a specific item
 	// consume a specific item
+	@Override
 	public void consume(String sku) {
 	public void consume(String sku) {
 		mPaymentManager.consume(sku);
 		mPaymentManager.consume(sku);
 	}
 	}
 
 
 	// query in app item detail info
 	// query in app item detail info
+	@Override
 	public void querySkuDetails(String[] list) {
 	public void querySkuDetails(String[] list) {
 		List<String> nKeys = Arrays.asList(list);
 		List<String> nKeys = Arrays.asList(list);
 		List<String> cKeys = Arrays.asList(mSkuDetails.get_keys());
 		List<String> cKeys = Arrays.asList(mSkuDetails.get_keys());
@@ -202,6 +225,7 @@ public class GodotPaymentV3 extends Godot.SingletonBase {
 		}
 		}
 	}
 	}
 
 
+	@Override
 	public void addSkuDetail(String itemJson) {
 	public void addSkuDetail(String itemJson) {
 		JSONObject o = null;
 		JSONObject o = null;
 		try {
 		try {
@@ -220,11 +244,25 @@ public class GodotPaymentV3 extends Godot.SingletonBase {
 		}
 		}
 	}
 	}
 
 
+	@Override
 	public void completeSkuDetail() {
 	public void completeSkuDetail() {
 		GodotLib.calldeferred(purchaseCallbackId, "sku_details_complete", new Object[] { mSkuDetails });
 		GodotLib.calldeferred(purchaseCallbackId, "sku_details_complete", new Object[] { mSkuDetails });
 	}
 	}
 
 
+	@Override
 	public void errorSkuDetail(String errorMessage) {
 	public void errorSkuDetail(String errorMessage) {
 		GodotLib.calldeferred(purchaseCallbackId, "sku_details_error", new Object[] { errorMessage });
 		GodotLib.calldeferred(purchaseCallbackId, "sku_details_error", new Object[] { errorMessage });
 	}
 	}
+
+	@NonNull
+	@Override
+	public String getPluginName() {
+		return "GodotPayment";
+	}
+
+	@NonNull
+	@Override
+	public List<String> getPluginMethods() {
+		return Arrays.asList("purchase", "setPurchaseCallbackId", "setPurchaseValidationUrlPrefix", "setTransactionId", "getSignature", "consumeUnconsumedPurchases", "requestPurchased", "setAutoConsume", "consume", "querySkuDetails", "isConnected");
+	}
 }
 }

+ 1 - 0
platform/android/java/settings.gradle

@@ -3,3 +3,4 @@ rootProject.name = "Godot"
 
 
 include ':app'
 include ':app'
 include ':lib'
 include ':lib'
+include ':plugins:godotpayment'

File diff suppressed because it is too large
+ 406 - 946
platform/android/java_godot_lib_jni.cpp


+ 28 - 30
platform/android/java_godot_lib_jni.h

@@ -37,36 +37,34 @@
 // These functions can be called from within JAVA and are the means by which our JAVA implementation calls back into our C++ code.
 // These functions can be called from within JAVA and are the means by which our JAVA implementation calls back into our C++ code.
 // See java/src/org/godotengine/godot/GodotLib.java for the JAVA side of this (yes that's why we have the long names)
 // See java/src/org/godotengine/godot/GodotLib.java for the JAVA side of this (yes that's why we have the long names)
 extern "C" {
 extern "C" {
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jobject obj, jobject activity, jobject p_asset_manager, jboolean p_use_apk_expansion);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jobject obj, jobject activity);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jobject obj, jobjectArray p_cmdline);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jobject obj, jint width, jint height);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *env, jobject obj, bool p_32_bits);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jobject obj);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jobject obj);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch(JNIEnv *env, jobject obj, jint ev, jint pointer, jint count, jintArray positions);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_hover(JNIEnv *env, jobject obj, jint p_type, jint p_x, jint p_y);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_doubletap(JNIEnv *env, jobject obj, jint p_x, jint p_y);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_scroll(JNIEnv *env, jobject obj, jint p_x, jint p_y);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jobject obj, jint p_scancode, jint p_unicode_char, jboolean p_pressed);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env, jobject obj, jint p_device, jint p_button, jboolean p_pressed);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, jobject obj, jint p_device, jint p_axis, jfloat p_value);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyhat(JNIEnv *env, jobject obj, jint p_device, jint p_hat_x, jint p_hat_y);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyconnectionchanged(JNIEnv *env, jobject obj, jint p_device, jboolean p_connected, jstring p_name);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_audio(JNIEnv *env, jobject obj);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_accelerometer(JNIEnv *env, jobject obj, jfloat x, jfloat y, jfloat z);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gravity(JNIEnv *env, jobject obj, jfloat x, jfloat y, jfloat z);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnetometer(JNIEnv *env, jobject obj, jfloat x, jfloat y, jfloat z);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gyroscope(JNIEnv *env, jobject obj, jfloat x, jfloat y, jfloat z);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env, jobject obj);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, jobject obj);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_singleton(JNIEnv *env, jobject obj, jstring name, jobject p_object);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_method(JNIEnv *env, jobject obj, jstring sname, jstring name, jstring ret, jobjectArray args);
-JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv *env, jobject obj, jstring path);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *env, jobject p_obj, jint ID, jstring method, jobjectArray params);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jobject p_obj, jint ID, jstring method, jobjectArray params);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jobject obj, jint p_height);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jobject p_obj, jstring p_permission, jboolean p_result);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject activity, jobject p_asset_manager, jboolean p_use_apk_expansion);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz, jobject activity);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jclass clazz, jint width, jint height);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *env, jclass clazz, jboolean p_32_bits);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jclass clazz);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint count, jintArray positions);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_hover(JNIEnv *env, jclass clazz, jint p_type, jint p_x, jint p_y);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_doubletap(JNIEnv *env, jclass clazz, jint p_x, jint p_y);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_scroll(JNIEnv *env, jclass clazz, jint p_x, jint p_y);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_scancode, jint p_unicode_char, jboolean p_pressed);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env, jclass clazz, jint p_device, jint p_button, jboolean p_pressed);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, jclass clazz, jint p_device, jint p_axis, jfloat p_value);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyhat(JNIEnv *env, jclass clazz, jint p_device, jint p_hat_x, jint p_hat_y);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyconnectionchanged(JNIEnv *env, jclass clazz, jint p_device, jboolean p_connected, jstring p_name);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_audio(JNIEnv *env, jclass clazz);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_accelerometer(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gravity(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnetometer(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gyroscope(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env, jclass clazz);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, jclass clazz);
+JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv *env, jclass clazz, jstring path);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *env, jclass clazz, jint ID, jstring method, jobjectArray params);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jclass clazz, jint ID, jstring method, jobjectArray params);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jclass clazz, jint p_height);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz);
 }
 }

+ 10 - 0
platform/android/java_godot_wrapper.cpp

@@ -66,6 +66,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance) {
 	_is_activity_resumed = p_env->GetMethodID(cls, "isActivityResumed", "()Z");
 	_is_activity_resumed = p_env->GetMethodID(cls, "isActivityResumed", "()Z");
 	_vibrate = p_env->GetMethodID(cls, "vibrate", "(I)V");
 	_vibrate = p_env->GetMethodID(cls, "vibrate", "(I)V");
 	_get_input_fallback_mapping = p_env->GetMethodID(cls, "getInputFallbackMapping", "()Ljava/lang/String;");
 	_get_input_fallback_mapping = p_env->GetMethodID(cls, "getInputFallbackMapping", "()Ljava/lang/String;");
+	_on_gl_godot_main_loop_started = p_env->GetMethodID(cls, "onGLGodotMainLoopStarted", "()V");
 }
 }
 
 
 GodotJavaWrapper::~GodotJavaWrapper() {
 GodotJavaWrapper::~GodotJavaWrapper() {
@@ -114,6 +115,15 @@ void GodotJavaWrapper::on_video_init(JNIEnv *p_env) {
 	p_env->CallVoidMethod(godot_instance, _on_video_init);
 	p_env->CallVoidMethod(godot_instance, _on_video_init);
 }
 }
 
 
+void GodotJavaWrapper::on_gl_godot_main_loop_started(JNIEnv *p_env) {
+	if (_on_gl_godot_main_loop_started) {
+		if (p_env == NULL) {
+			p_env = ThreadAndroid::get_env();
+		}
+	}
+	p_env->CallVoidMethod(godot_instance, _on_gl_godot_main_loop_started);
+}
+
 void GodotJavaWrapper::restart(JNIEnv *p_env) {
 void GodotJavaWrapper::restart(JNIEnv *p_env) {
 	if (_restart)
 	if (_restart)
 		if (p_env == NULL)
 		if (p_env == NULL)

+ 2 - 0
platform/android/java_godot_wrapper.h

@@ -61,6 +61,7 @@ private:
 	jmethodID _is_activity_resumed = 0;
 	jmethodID _is_activity_resumed = 0;
 	jmethodID _vibrate = 0;
 	jmethodID _vibrate = 0;
 	jmethodID _get_input_fallback_mapping = 0;
 	jmethodID _get_input_fallback_mapping = 0;
+	jmethodID _on_gl_godot_main_loop_started = 0;
 
 
 public:
 public:
 	GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance);
 	GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance);
@@ -73,6 +74,7 @@ public:
 
 
 	void gfx_init(bool gl2);
 	void gfx_init(bool gl2);
 	void on_video_init(JNIEnv *p_env = NULL);
 	void on_video_init(JNIEnv *p_env = NULL);
+	void on_gl_godot_main_loop_started(JNIEnv *p_env = NULL);
 	void restart(JNIEnv *p_env = NULL);
 	void restart(JNIEnv *p_env = NULL);
 	void force_quit(JNIEnv *p_env = NULL);
 	void force_quit(JNIEnv *p_env = NULL);
 	void set_keep_screen_on(bool p_enabled);
 	void set_keep_screen_on(bool p_enabled);

+ 432 - 0
platform/android/jni_utils.cpp

@@ -0,0 +1,432 @@
+/*************************************************************************/
+/*  jni_utils.cpp                                                        */
+/*************************************************************************/
+/*                       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.                */
+/*************************************************************************/
+
+#include "jni_utils.h"
+
+jvalret _variant_to_jvalue(JNIEnv *env, Variant::Type p_type, const Variant *p_arg, bool force_jobject) {
+
+	jvalret v;
+
+	switch (p_type) {
+
+		case Variant::BOOL: {
+
+			if (force_jobject) {
+				jclass bclass = env->FindClass("java/lang/Boolean");
+				jmethodID ctor = env->GetMethodID(bclass, "<init>", "(Z)V");
+				jvalue val;
+				val.z = (bool)(*p_arg);
+				jobject obj = env->NewObjectA(bclass, ctor, &val);
+				v.val.l = obj;
+				v.obj = obj;
+				env->DeleteLocalRef(bclass);
+			} else {
+				v.val.z = *p_arg;
+			};
+		} break;
+		case Variant::INT: {
+
+			if (force_jobject) {
+
+				jclass bclass = env->FindClass("java/lang/Integer");
+				jmethodID ctor = env->GetMethodID(bclass, "<init>", "(I)V");
+				jvalue val;
+				val.i = (int)(*p_arg);
+				jobject obj = env->NewObjectA(bclass, ctor, &val);
+				v.val.l = obj;
+				v.obj = obj;
+				env->DeleteLocalRef(bclass);
+
+			} else {
+				v.val.i = *p_arg;
+			};
+		} break;
+		case Variant::REAL: {
+
+			if (force_jobject) {
+
+				jclass bclass = env->FindClass("java/lang/Double");
+				jmethodID ctor = env->GetMethodID(bclass, "<init>", "(D)V");
+				jvalue val;
+				val.d = (double)(*p_arg);
+				jobject obj = env->NewObjectA(bclass, ctor, &val);
+				v.val.l = obj;
+				v.obj = obj;
+				env->DeleteLocalRef(bclass);
+
+			} else {
+				v.val.f = *p_arg;
+			};
+		} break;
+		case Variant::STRING: {
+
+			String s = *p_arg;
+			jstring jStr = env->NewStringUTF(s.utf8().get_data());
+			v.val.l = jStr;
+			v.obj = jStr;
+		} break;
+		case Variant::POOL_STRING_ARRAY: {
+
+			PoolVector<String> sarray = *p_arg;
+			jobjectArray arr = env->NewObjectArray(sarray.size(), env->FindClass("java/lang/String"), env->NewStringUTF(""));
+
+			for (int j = 0; j < sarray.size(); j++) {
+
+				jstring str = env->NewStringUTF(sarray[j].utf8().get_data());
+				env->SetObjectArrayElement(arr, j, str);
+				env->DeleteLocalRef(str);
+			}
+			v.val.l = arr;
+			v.obj = arr;
+
+		} break;
+
+		case Variant::DICTIONARY: {
+
+			Dictionary dict = *p_arg;
+			jclass dclass = env->FindClass("org/godotengine/godot/Dictionary");
+			jmethodID ctor = env->GetMethodID(dclass, "<init>", "()V");
+			jobject jdict = env->NewObject(dclass, ctor);
+
+			Array keys = dict.keys();
+
+			jobjectArray jkeys = env->NewObjectArray(keys.size(), env->FindClass("java/lang/String"), env->NewStringUTF(""));
+			for (int j = 0; j < keys.size(); j++) {
+				jstring str = env->NewStringUTF(String(keys[j]).utf8().get_data());
+				env->SetObjectArrayElement(jkeys, j, str);
+				env->DeleteLocalRef(str);
+			};
+
+			jmethodID set_keys = env->GetMethodID(dclass, "set_keys", "([Ljava/lang/String;)V");
+			jvalue val;
+			val.l = jkeys;
+			env->CallVoidMethodA(jdict, set_keys, &val);
+			env->DeleteLocalRef(jkeys);
+
+			jobjectArray jvalues = env->NewObjectArray(keys.size(), env->FindClass("java/lang/Object"), NULL);
+
+			for (int j = 0; j < keys.size(); j++) {
+				Variant var = dict[keys[j]];
+				jvalret v = _variant_to_jvalue(env, var.get_type(), &var, true);
+				env->SetObjectArrayElement(jvalues, j, v.val.l);
+				if (v.obj) {
+					env->DeleteLocalRef(v.obj);
+				}
+			};
+
+			jmethodID set_values = env->GetMethodID(dclass, "set_values", "([Ljava/lang/Object;)V");
+			val.l = jvalues;
+			env->CallVoidMethodA(jdict, set_values, &val);
+			env->DeleteLocalRef(jvalues);
+			env->DeleteLocalRef(dclass);
+
+			v.val.l = jdict;
+			v.obj = jdict;
+		} break;
+
+		case Variant::POOL_INT_ARRAY: {
+
+			PoolVector<int> array = *p_arg;
+			jintArray arr = env->NewIntArray(array.size());
+			PoolVector<int>::Read r = array.read();
+			env->SetIntArrayRegion(arr, 0, array.size(), r.ptr());
+			v.val.l = arr;
+			v.obj = arr;
+
+		} break;
+		case Variant::POOL_BYTE_ARRAY: {
+			PoolVector<uint8_t> array = *p_arg;
+			jbyteArray arr = env->NewByteArray(array.size());
+			PoolVector<uint8_t>::Read r = array.read();
+			env->SetByteArrayRegion(arr, 0, array.size(), reinterpret_cast<const signed char *>(r.ptr()));
+			v.val.l = arr;
+			v.obj = arr;
+
+		} break;
+		case Variant::POOL_REAL_ARRAY: {
+
+			PoolVector<float> array = *p_arg;
+			jfloatArray arr = env->NewFloatArray(array.size());
+			PoolVector<float>::Read r = array.read();
+			env->SetFloatArrayRegion(arr, 0, array.size(), r.ptr());
+			v.val.l = arr;
+			v.obj = arr;
+
+		} break;
+		default: {
+
+			v.val.i = 0;
+		} break;
+	}
+	return v;
+}
+
+String _get_class_name(JNIEnv *env, jclass cls, bool *array) {
+
+	jclass cclass = env->FindClass("java/lang/Class");
+	jmethodID getName = env->GetMethodID(cclass, "getName", "()Ljava/lang/String;");
+	jstring clsName = (jstring)env->CallObjectMethod(cls, getName);
+
+	if (array) {
+		jmethodID isArray = env->GetMethodID(cclass, "isArray", "()Z");
+		jboolean isarr = env->CallBooleanMethod(cls, isArray);
+		(*array) = isarr ? true : false;
+	}
+	String name = jstring_to_string(clsName, env);
+	env->DeleteLocalRef(clsName);
+
+	return name;
+}
+
+Variant _jobject_to_variant(JNIEnv *env, jobject obj) {
+
+	if (obj == NULL) {
+		return Variant();
+	}
+
+	jclass c = env->GetObjectClass(obj);
+	bool array;
+	String name = _get_class_name(env, c, &array);
+
+	if (name == "java.lang.String") {
+
+		return jstring_to_string((jstring)obj, env);
+	};
+
+	if (name == "[Ljava.lang.String;") {
+
+		jobjectArray arr = (jobjectArray)obj;
+		int stringCount = env->GetArrayLength(arr);
+		PoolVector<String> sarr;
+
+		for (int i = 0; i < stringCount; i++) {
+			jstring string = (jstring)env->GetObjectArrayElement(arr, i);
+			sarr.push_back(jstring_to_string(string, env));
+			env->DeleteLocalRef(string);
+		}
+
+		return sarr;
+	};
+
+	if (name == "java.lang.Boolean") {
+
+		jmethodID boolValue = env->GetMethodID(c, "booleanValue", "()Z");
+		bool ret = env->CallBooleanMethod(obj, boolValue);
+		return ret;
+	};
+
+	if (name == "java.lang.Integer" || name == "java.lang.Long") {
+
+		jclass nclass = env->FindClass("java/lang/Number");
+		jmethodID longValue = env->GetMethodID(nclass, "longValue", "()J");
+		jlong ret = env->CallLongMethod(obj, longValue);
+		return ret;
+	};
+
+	if (name == "[I") {
+
+		jintArray arr = (jintArray)obj;
+		int fCount = env->GetArrayLength(arr);
+		PoolVector<int> sarr;
+		sarr.resize(fCount);
+
+		PoolVector<int>::Write w = sarr.write();
+		env->GetIntArrayRegion(arr, 0, fCount, w.ptr());
+		w.release();
+		return sarr;
+	};
+
+	if (name == "[B") {
+
+		jbyteArray arr = (jbyteArray)obj;
+		int fCount = env->GetArrayLength(arr);
+		PoolVector<uint8_t> sarr;
+		sarr.resize(fCount);
+
+		PoolVector<uint8_t>::Write w = sarr.write();
+		env->GetByteArrayRegion(arr, 0, fCount, reinterpret_cast<signed char *>(w.ptr()));
+		w.release();
+		return sarr;
+	};
+
+	if (name == "java.lang.Float" || name == "java.lang.Double") {
+
+		jclass nclass = env->FindClass("java/lang/Number");
+		jmethodID doubleValue = env->GetMethodID(nclass, "doubleValue", "()D");
+		double ret = env->CallDoubleMethod(obj, doubleValue);
+		return ret;
+	};
+
+	if (name == "[D") {
+
+		jdoubleArray arr = (jdoubleArray)obj;
+		int fCount = env->GetArrayLength(arr);
+		PoolRealArray sarr;
+		sarr.resize(fCount);
+
+		PoolRealArray::Write w = sarr.write();
+
+		for (int i = 0; i < fCount; i++) {
+
+			double n;
+			env->GetDoubleArrayRegion(arr, i, 1, &n);
+			w.ptr()[i] = n;
+		};
+		return sarr;
+	};
+
+	if (name == "[F") {
+
+		jfloatArray arr = (jfloatArray)obj;
+		int fCount = env->GetArrayLength(arr);
+		PoolRealArray sarr;
+		sarr.resize(fCount);
+
+		PoolRealArray::Write w = sarr.write();
+
+		for (int i = 0; i < fCount; i++) {
+
+			float n;
+			env->GetFloatArrayRegion(arr, i, 1, &n);
+			w.ptr()[i] = n;
+		};
+		return sarr;
+	};
+
+	if (name == "[Ljava.lang.Object;") {
+
+		jobjectArray arr = (jobjectArray)obj;
+		int objCount = env->GetArrayLength(arr);
+		Array varr;
+
+		for (int i = 0; i < objCount; i++) {
+			jobject jobj = env->GetObjectArrayElement(arr, i);
+			Variant v = _jobject_to_variant(env, jobj);
+			varr.push_back(v);
+			env->DeleteLocalRef(jobj);
+		}
+
+		return varr;
+	};
+
+	if (name == "java.util.HashMap" || name == "org.godotengine.godot.Dictionary") {
+
+		Dictionary ret;
+		jclass oclass = c;
+		jmethodID get_keys = env->GetMethodID(oclass, "get_keys", "()[Ljava/lang/String;");
+		jobjectArray arr = (jobjectArray)env->CallObjectMethod(obj, get_keys);
+
+		PoolStringArray keys = _jobject_to_variant(env, arr);
+		env->DeleteLocalRef(arr);
+
+		jmethodID get_values = env->GetMethodID(oclass, "get_values", "()[Ljava/lang/Object;");
+		arr = (jobjectArray)env->CallObjectMethod(obj, get_values);
+
+		Array vals = _jobject_to_variant(env, arr);
+		env->DeleteLocalRef(arr);
+
+		for (int i = 0; i < keys.size(); i++) {
+
+			ret[keys[i]] = vals[i];
+		};
+
+		return ret;
+	};
+
+	env->DeleteLocalRef(c);
+
+	return Variant();
+}
+
+Variant::Type get_jni_type(const String &p_type) {
+
+	static struct {
+		const char *name;
+		Variant::Type type;
+	} _type_to_vtype[] = {
+		{ "void", Variant::NIL },
+		{ "boolean", Variant::BOOL },
+		{ "int", Variant::INT },
+		{ "float", Variant::REAL },
+		{ "double", Variant::REAL },
+		{ "java.lang.String", Variant::STRING },
+		{ "[I", Variant::POOL_INT_ARRAY },
+		{ "[B", Variant::POOL_BYTE_ARRAY },
+		{ "[F", Variant::POOL_REAL_ARRAY },
+		{ "[Ljava.lang.String;", Variant::POOL_STRING_ARRAY },
+		{ "org.godotengine.godot.Dictionary", Variant::DICTIONARY },
+		{ NULL, Variant::NIL }
+	};
+
+	int idx = 0;
+
+	while (_type_to_vtype[idx].name) {
+
+		if (p_type == _type_to_vtype[idx].name)
+			return _type_to_vtype[idx].type;
+
+		idx++;
+	}
+
+	return Variant::NIL;
+}
+
+const char *get_jni_sig(const String &p_type) {
+
+	static struct {
+		const char *name;
+		const char *sig;
+	} _type_to_vtype[] = {
+		{ "void", "V" },
+		{ "boolean", "Z" },
+		{ "int", "I" },
+		{ "float", "F" },
+		{ "double", "D" },
+		{ "java.lang.String", "Ljava/lang/String;" },
+		{ "org.godotengine.godot.Dictionary", "Lorg/godotengine/godot/Dictionary;" },
+		{ "[I", "[I" },
+		{ "[B", "[B" },
+		{ "[F", "[F" },
+		{ "[Ljava.lang.String;", "[Ljava/lang/String;" },
+		{ NULL, "V" }
+	};
+
+	int idx = 0;
+
+	while (_type_to_vtype[idx].name) {
+
+		if (p_type == _type_to_vtype[idx].name)
+			return _type_to_vtype[idx].sig;
+
+		idx++;
+	}
+
+	return "Ljava/lang/Object;";
+}

+ 55 - 0
platform/android/jni_utils.h

@@ -0,0 +1,55 @@
+/*************************************************************************/
+/*  jni_utils.h                                                          */
+/*************************************************************************/
+/*                       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.                */
+/*************************************************************************/
+
+#ifndef JNI_UTILS_H
+#define JNI_UTILS_H
+
+#include "string_android.h"
+#include <core/variant.h>
+#include <jni.h>
+
+struct jvalret {
+
+	jobject obj;
+	jvalue val;
+	jvalret() { obj = NULL; }
+};
+
+jvalret _variant_to_jvalue(JNIEnv *env, Variant::Type p_type, const Variant *p_arg, bool force_jobject = false);
+
+String _get_class_name(JNIEnv *env, jclass cls, bool *array);
+
+Variant _jobject_to_variant(JNIEnv *env, jobject obj);
+
+Variant::Type get_jni_type(const String &p_type);
+
+const char *get_jni_sig(const String &p_type);
+
+#endif // JNI_UTILS_H

+ 300 - 0
platform/android/plugin/godot_plugin_jni.cpp

@@ -0,0 +1,300 @@
+/*************************************************************************/
+/*  godot_plugin_jni.cpp                                                 */
+/*************************************************************************/
+/*                       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.                */
+/*************************************************************************/
+
+#include "godot_plugin_jni.h"
+
+#include <core/engine.h>
+#include <core/error_macros.h>
+#include <core/project_settings.h>
+#include <platform/android/jni_utils.h>
+#include <platform/android/string_android.h>
+
+class JNISingleton : public Object {
+
+	GDCLASS(JNISingleton, Object);
+
+	struct MethodData {
+
+		jmethodID method;
+		Variant::Type ret_type;
+		Vector<Variant::Type> argtypes;
+	};
+
+	jobject instance;
+	Map<StringName, MethodData> method_map;
+
+public:
+	virtual Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Variant::CallError &r_error) {
+
+		ERR_FAIL_COND_V(!instance, Variant());
+
+		r_error.error = Variant::CallError::CALL_OK;
+
+		Map<StringName, MethodData>::Element *E = method_map.find(p_method);
+		if (!E) {
+
+			r_error.error = Variant::CallError::CALL_ERROR_INVALID_METHOD;
+			return Variant();
+		}
+
+		int ac = E->get().argtypes.size();
+		if (ac < p_argcount) {
+
+			r_error.error = Variant::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS;
+			r_error.argument = ac;
+			return Variant();
+		}
+
+		if (ac > p_argcount) {
+
+			r_error.error = Variant::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS;
+			r_error.argument = ac;
+			return Variant();
+		}
+
+		for (int i = 0; i < p_argcount; i++) {
+
+			if (!Variant::can_convert(p_args[i]->get_type(), E->get().argtypes[i])) {
+
+				r_error.error = Variant::CallError::CALL_ERROR_INVALID_ARGUMENT;
+				r_error.argument = i;
+				r_error.expected = E->get().argtypes[i];
+			}
+		}
+
+		jvalue *v = NULL;
+
+		if (p_argcount) {
+
+			v = (jvalue *)alloca(sizeof(jvalue) * p_argcount);
+		}
+
+		JNIEnv *env = ThreadAndroid::get_env();
+
+		int res = env->PushLocalFrame(16);
+
+		ERR_FAIL_COND_V(res != 0, Variant());
+
+		List<jobject> to_erase;
+		for (int i = 0; i < p_argcount; i++) {
+
+			jvalret vr = _variant_to_jvalue(env, E->get().argtypes[i], p_args[i]);
+			v[i] = vr.val;
+			if (vr.obj)
+				to_erase.push_back(vr.obj);
+		}
+
+		Variant ret;
+
+		switch (E->get().ret_type) {
+
+			case Variant::NIL: {
+
+				env->CallVoidMethodA(instance, E->get().method, v);
+			} break;
+			case Variant::BOOL: {
+
+				ret = env->CallBooleanMethodA(instance, E->get().method, v) == JNI_TRUE;
+			} break;
+			case Variant::INT: {
+
+				ret = env->CallIntMethodA(instance, E->get().method, v);
+			} break;
+			case Variant::REAL: {
+
+				ret = env->CallFloatMethodA(instance, E->get().method, v);
+			} break;
+			case Variant::STRING: {
+
+				jobject o = env->CallObjectMethodA(instance, E->get().method, v);
+				ret = jstring_to_string((jstring)o, env);
+				env->DeleteLocalRef(o);
+			} break;
+			case Variant::POOL_STRING_ARRAY: {
+
+				jobjectArray arr = (jobjectArray)env->CallObjectMethodA(instance, E->get().method, v);
+
+				ret = _jobject_to_variant(env, arr);
+
+				env->DeleteLocalRef(arr);
+			} break;
+			case Variant::POOL_INT_ARRAY: {
+
+				jintArray arr = (jintArray)env->CallObjectMethodA(instance, E->get().method, v);
+
+				int fCount = env->GetArrayLength(arr);
+				PoolVector<int> sarr;
+				sarr.resize(fCount);
+
+				PoolVector<int>::Write w = sarr.write();
+				env->GetIntArrayRegion(arr, 0, fCount, w.ptr());
+				w.release();
+				ret = sarr;
+				env->DeleteLocalRef(arr);
+			} break;
+			case Variant::POOL_REAL_ARRAY: {
+
+				jfloatArray arr = (jfloatArray)env->CallObjectMethodA(instance, E->get().method, v);
+
+				int fCount = env->GetArrayLength(arr);
+				PoolVector<float> sarr;
+				sarr.resize(fCount);
+
+				PoolVector<float>::Write w = sarr.write();
+				env->GetFloatArrayRegion(arr, 0, fCount, w.ptr());
+				w.release();
+				ret = sarr;
+				env->DeleteLocalRef(arr);
+			} break;
+
+			case Variant::DICTIONARY: {
+
+				jobject obj = env->CallObjectMethodA(instance, E->get().method, v);
+				ret = _jobject_to_variant(env, obj);
+				env->DeleteLocalRef(obj);
+
+			} break;
+			default: {
+
+				env->PopLocalFrame(NULL);
+				ERR_FAIL_V(Variant());
+			} break;
+		}
+
+		while (to_erase.size()) {
+			env->DeleteLocalRef(to_erase.front()->get());
+			to_erase.pop_front();
+		}
+
+		env->PopLocalFrame(NULL);
+
+		return ret;
+	}
+
+	jobject get_instance() const {
+
+		return instance;
+	}
+	void set_instance(jobject p_instance) {
+
+		instance = p_instance;
+	}
+
+	void add_method(const StringName &p_name, jmethodID p_method, const Vector<Variant::Type> &p_args, Variant::Type p_ret_type) {
+
+		MethodData md;
+		md.method = p_method;
+		md.argtypes = p_args;
+		md.ret_type = p_ret_type;
+		method_map[p_name] = md;
+	}
+
+	JNISingleton() {
+		instance = NULL;
+	}
+};
+
+static HashMap<String, JNISingleton *> jni_singletons;
+
+extern "C" {
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterSingleton(JNIEnv *env, jclass clazz, jstring name, jobject obj) {
+
+	String singname = jstring_to_string(name, env);
+	JNISingleton *s = memnew(JNISingleton);
+	s->set_instance(env->NewGlobalRef(obj));
+	jni_singletons[singname] = s;
+
+	Engine::get_singleton()->add_singleton(Engine::Singleton(singname, s));
+	ProjectSettings::get_singleton()->set(singname, s);
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterMethod(JNIEnv *env, jclass clazz, jstring sname, jstring name, jstring ret, jobjectArray args) {
+
+	String singname = jstring_to_string(sname, env);
+
+	ERR_FAIL_COND(!jni_singletons.has(singname));
+
+	JNISingleton *s = jni_singletons.get(singname);
+
+	String mname = jstring_to_string(name, env);
+	String retval = jstring_to_string(ret, env);
+	Vector<Variant::Type> types;
+	String cs = "(";
+
+	int stringCount = env->GetArrayLength(args);
+
+	for (int i = 0; i < stringCount; i++) {
+
+		jstring string = (jstring)env->GetObjectArrayElement(args, i);
+		const String rawString = jstring_to_string(string, env);
+		types.push_back(get_jni_type(rawString));
+		cs += get_jni_sig(rawString);
+	}
+
+	cs += ")";
+	cs += get_jni_sig(retval);
+	jclass cls = env->GetObjectClass(s->get_instance());
+	jmethodID mid = env->GetMethodID(cls, mname.ascii().get_data(), cs.ascii().get_data());
+	if (!mid) {
+
+		print_line("Failed getting method ID " + mname);
+	}
+
+	s->add_method(mname, mid, types, get_jni_type(retval));
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterGDNativeLibraries(JNIEnv *env, jobject obj, jobjectArray gdnlib_paths) {
+	int gdnlib_count = env->GetArrayLength(gdnlib_paths);
+	if (gdnlib_count == 0) {
+		return;
+	}
+
+	// Retrieve the current list of gdnative libraries.
+	Array singletons = Array();
+	if (ProjectSettings::get_singleton()->has_setting("gdnative/singletons")) {
+		singletons = ProjectSettings::get_singleton()->get("gdnative/singletons");
+	}
+
+	// Insert the libraries provided by the plugin
+	for (int i = 0; i < gdnlib_count; i++) {
+		jstring relative_path = (jstring)env->GetObjectArrayElement(gdnlib_paths, i);
+
+		String path = "res://" + jstring_to_string(relative_path, env);
+		if (!singletons.has(path)) {
+			singletons.push_back(path);
+		}
+		env->DeleteLocalRef(relative_path);
+	}
+
+	// Insert the updated list back into project settings.
+	ProjectSettings::get_singleton()->set("gdnative/singletons", singletons);
+}
+}

+ 43 - 0
platform/android/plugin/godot_plugin_jni.h

@@ -0,0 +1,43 @@
+/*************************************************************************/
+/*  godot_plugin_jni.h                                                   */
+/*************************************************************************/
+/*                       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.                */
+/*************************************************************************/
+
+#ifndef GODOT_PLUGIN_JNI_H
+#define GODOT_PLUGIN_JNI_H
+
+#include <android/log.h>
+#include <jni.h>
+
+extern "C" {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterSingleton(JNIEnv *env, jclass clazz, jstring name, jobject obj);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterMethod(JNIEnv *env, jclass clazz, jstring sname, jstring name, jstring ret, jobjectArray args);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterGDNativeLibraries(JNIEnv *env, jobject obj, jobjectArray gdnlib_paths);
+}
+
+#endif // GODOT_PLUGIN_JNI_H

Some files were not shown because too many files changed in this diff