Browse Source

Merge pull request #57747 from m4gr3d/android_editor_3x

[3.x] Editor Android Port
Rémi Verschelde 3 years ago
parent
commit
11d40d76a3

+ 3 - 0
editor/editor_node.cpp

@@ -474,6 +474,9 @@ void EditorNode::_notification(int p_what) {
 			get_tree()->get_root()->set_as_audio_listener(false);
 			get_tree()->get_root()->set_as_audio_listener(false);
 			get_tree()->get_root()->set_as_audio_listener_2d(false);
 			get_tree()->get_root()->set_as_audio_listener_2d(false);
 			get_tree()->set_auto_accept_quit(false);
 			get_tree()->set_auto_accept_quit(false);
+#ifdef ANDROID_ENABLED
+			get_tree()->set_quit_on_go_back(false);
+#endif
 			get_tree()->connect("files_dropped", this, "_dropped_files");
 			get_tree()->connect("files_dropped", this, "_dropped_files");
 			get_tree()->connect("global_menu_action", this, "_global_menu_action");
 			get_tree()->connect("global_menu_action", this, "_global_menu_action");
 
 

+ 9 - 2
platform/android/SCsub

@@ -50,10 +50,17 @@ else:
 if lib_arch_dir != "":
 if lib_arch_dir != "":
     if env["target"] == "release":
     if env["target"] == "release":
         lib_type_dir = "release"
         lib_type_dir = "release"
-    else:  # release_debug, debug
+    elif env["target"] == "release_debug":
         lib_type_dir = "debug"
         lib_type_dir = "debug"
+    else:  # debug
+        lib_type_dir = "dev"
 
 
-    out_dir = "#platform/android/java/lib/libs/" + lib_type_dir + "/" + lib_arch_dir
+    if env["tools"]:
+        lib_tools_dir = "tools/"
+    else:
+        lib_tools_dir = ""
+
+    out_dir = "#platform/android/java/lib/libs/" + lib_tools_dir + lib_type_dir + "/" + lib_arch_dir
     env_android.Command(
     env_android.Command(
         out_dir + "/libgodot_android.so", "#bin/libgodot" + env["SHLIBSUFFIX"], Move("$TARGET", "$SOURCE")
         out_dir + "/libgodot_android.so", "#bin/libgodot" + env["SHLIBSUFFIX"], Move("$TARGET", "$SOURCE")
     )
     )

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

@@ -42,6 +42,11 @@ allprojects {
     }
     }
 }
 }
 
 
+configurations {
+    // Initializes a placeholder for the devImplementation dependency configuration.
+    devImplementation {}
+}
+
 dependencies {
 dependencies {
     implementation libraries.kotlinStdLib
     implementation libraries.kotlinStdLib
     implementation libraries.androidxFragment
     implementation libraries.androidxFragment
@@ -54,6 +59,7 @@ dependencies {
         // 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'])
+        devImplementation fileTree(dir: 'libs/dev', include: ['*.jar', '*.aar'])
         releaseImplementation fileTree(dir: 'libs/release', include: ['*.jar', '*.aar'])
         releaseImplementation fileTree(dir: 'libs/release', include: ['*.jar', '*.aar'])
     }
     }
 
 
@@ -78,6 +84,7 @@ dependencies {
 android {
 android {
     compileSdkVersion versions.compileSdk
     compileSdkVersion versions.compileSdk
     buildToolsVersion versions.buildTools
     buildToolsVersion versions.buildTools
+    ndkVersion versions.ndkVersion
 
 
     compileOptions {
     compileOptions {
         sourceCompatibility versions.javaVersion
         sourceCompatibility versions.javaVersion
@@ -105,6 +112,8 @@ android {
         versionName getExportVersionName()
         versionName getExportVersionName()
         minSdkVersion getExportMinSdkVersion()
         minSdkVersion getExportMinSdkVersion()
         targetSdkVersion getExportTargetSdkVersion()
         targetSdkVersion getExportTargetSdkVersion()
+
+        missingDimensionStrategy 'products', 'template'
 //CHUNK_ANDROID_DEFAULTCONFIG_BEGIN
 //CHUNK_ANDROID_DEFAULTCONFIG_BEGIN
 //CHUNK_ANDROID_DEFAULTCONFIG_END
 //CHUNK_ANDROID_DEFAULTCONFIG_END
     }
     }
@@ -160,6 +169,18 @@ android {
             }
             }
         }
         }
 
 
+        dev {
+            initWith debug
+            // Signing and zip-aligning are skipped for prebuilt builds, but
+            // performed for custom builds.
+            zipAlignEnabled shouldZipAlign()
+            if (shouldSign()) {
+                signingConfig signingConfigs.debug
+            } else {
+                signingConfig null
+            }
+        }
+
         release {
         release {
             // Signing and zip-aligning are skipped for prebuilt builds, but
             // Signing and zip-aligning are skipped for prebuilt builds, but
             // performed for custom builds.
             // performed for custom builds.
@@ -200,6 +221,11 @@ android {
             'libs/debug'
             'libs/debug'
 //DIR_JNI_DEBUG_BEGIN
 //DIR_JNI_DEBUG_BEGIN
 //DIR_JNI_DEBUG_END
 //DIR_JNI_DEBUG_END
+        ]
+        dev.jniLibs.srcDirs = [
+            'libs/dev'
+//DIR_JNI_DEV_BEGIN
+//DIR_JNI_DEV_END
         ]
         ]
         release.jniLibs.srcDirs = [
         release.jniLibs.srcDirs = [
             'libs/release'
             'libs/release'
@@ -221,6 +247,12 @@ task copyAndRenameDebugApk(type: Copy) {
     rename "android_debug.apk", getExportFilename()
     rename "android_debug.apk", getExportFilename()
 }
 }
 
 
+task copyAndRenameDevApk(type: Copy) {
+    from "$buildDir/outputs/apk/dev/android_dev.apk"
+    into getExportPath()
+    rename "android_dev.apk", getExportFilename()
+}
+
 task copyAndRenameReleaseApk(type: Copy) {
 task copyAndRenameReleaseApk(type: Copy) {
     from "$buildDir/outputs/apk/release/android_release.apk"
     from "$buildDir/outputs/apk/release/android_release.apk"
     into getExportPath()
     into getExportPath()
@@ -233,6 +265,12 @@ task copyAndRenameDebugAab(type: Copy) {
     rename "build-debug.aab", getExportFilename()
     rename "build-debug.aab", getExportFilename()
 }
 }
 
 
+task copyAndRenameDevAab(type: Copy) {
+    from "$buildDir/outputs/bundle/dev/build-dev.aab"
+    into getExportPath()
+    rename "build-dev.aab", getExportFilename()
+}
+
 task copyAndRenameReleaseAab(type: Copy) {
 task copyAndRenameReleaseAab(type: Copy) {
     from "$buildDir/outputs/bundle/release/build-release.aab"
     from "$buildDir/outputs/bundle/release/build-release.aab"
     into getExportPath()
     into getExportPath()

+ 45 - 7
platform/android/java/app/config.gradle

@@ -76,7 +76,7 @@ ext.getGodotEditorVersion = { ->
     String editorVersion = project.hasProperty("godot_editor_version") ? project.property("godot_editor_version") : ""
     String editorVersion = project.hasProperty("godot_editor_version") ? project.property("godot_editor_version") : ""
     if (editorVersion == null || editorVersion.isEmpty()) {
     if (editorVersion == null || editorVersion.isEmpty()) {
         // Try the library version first
         // Try the library version first
-        editorVersion = getGodotLibraryVersion()
+        editorVersion = getGodotLibraryVersionName()
 
 
         if (editorVersion.isEmpty()) {
         if (editorVersion.isEmpty()) {
             // Fallback value.
             // Fallback value.
@@ -86,9 +86,24 @@ ext.getGodotEditorVersion = { ->
     return editorVersion
     return editorVersion
 }
 }
 
 
+ext.getGodotLibraryVersionCode = { ->
+    String versionName = ""
+    int versionCode = 1
+    (versionName, versionCode) = getGodotLibraryVersion()
+    return versionCode
+}
+
+ext.getGodotLibraryVersionName = { ->
+    String versionName = ""
+    int versionCode = 1
+    (versionName, versionCode) = getGodotLibraryVersion()
+    return versionName
+}
+
 ext.generateGodotLibraryVersion = { List<String> requiredKeys ->
 ext.generateGodotLibraryVersion = { List<String> requiredKeys ->
     // Attempt to read the version from the `version.py` file.
     // Attempt to read the version from the `version.py` file.
-    String libraryVersion = ""
+    String libraryVersionName = ""
+    int libraryVersionCode = 0
 
 
     File versionFile = new File("../../../version.py")
     File versionFile = new File("../../../version.py")
     if (versionFile.isFile()) {
     if (versionFile.isFile()) {
@@ -109,15 +124,35 @@ ext.generateGodotLibraryVersion = { List<String> requiredKeys ->
         }
         }
 
 
         if (requiredKeys.empty) {
         if (requiredKeys.empty) {
-            libraryVersion = map.values().join(".")
+            libraryVersionName = map.values().join(".")
+            try {
+                if (map.containsKey("patch")) {
+                    libraryVersionCode = Integer.parseInt(map["patch"])
+                }
+
+                if (map.containsKey("minor")) {
+                    libraryVersionCode += (Integer.parseInt(map["minor"]) * 100)
+                }
+
+                if (map.containsKey("major")) {
+                    libraryVersionCode += (Integer.parseInt(map["major"]) * 10000)
+                }
+            } catch (NumberFormatException ignore) {
+                libraryVersionCode = 1
+            }
         }
         }
     }
     }
 
 
-    if (libraryVersion.isEmpty()) {
+    if (libraryVersionName.isEmpty()) {
         // Fallback value in case we're unable to read the file.
         // Fallback value in case we're unable to read the file.
-        libraryVersion = "custom_build"
+        libraryVersionName = "custom_build"
+    }
+
+    if (libraryVersionCode == 0) {
+        libraryVersionCode = 1
     }
     }
-    return libraryVersion
+
+    return [libraryVersionName, libraryVersionCode]
 }
 }
 
 
 ext.getGodotLibraryVersion = { ->
 ext.getGodotLibraryVersion = { ->
@@ -127,7 +162,10 @@ ext.getGodotLibraryVersion = { ->
 
 
 ext.getGodotPublishVersion = { ->
 ext.getGodotPublishVersion = { ->
     List<String> requiredKeys = ["major", "minor", "patch", "status"]
     List<String> requiredKeys = ["major", "minor", "patch", "status"]
-    return generateGodotLibraryVersion(requiredKeys)
+    String versionName = ""
+    int versionCode = 1
+    (versionName, versionCode) = generateGodotLibraryVersion(requiredKeys)
+    return versionName
 }
 }
 
 
 final String VALUE_SEPARATOR_REGEX = "\\|"
 final String VALUE_SEPARATOR_REGEX = "\\|"

+ 127 - 17
platform/android/java/build.gradle

@@ -26,21 +26,22 @@ allprojects {
 
 
 ext {
 ext {
     supportedAbis = ["armv7", "arm64v8", "x86", "x86_64"]
     supportedAbis = ["armv7", "arm64v8", "x86", "x86_64"]
-    supportedTargets = ["release", "debug"]
+    supportedTargetsMap = [release: "release", dev: "debug", debug: "release_debug"]
+    supportedFlavors = ["editor", "template"]
 
 
-    // Used by gradle to specify which architecture to build for by default when running `./gradlew build`.
-    // This command is usually used by Android Studio.
+    // Used by gradle to specify which architecture to build for by default when running
+    // `./gradlew build` (this command is usually used by Android Studio).
     // If building manually on the command line, it's recommended to use the
     // If building manually on the command line, it's recommended to use the
-    // `./gradlew generateGodotTemplates` build command instead after running the `scons` command.
-    // The defaultAbi must be one of the {supportedAbis} values.
-    defaultAbi = "arm64v8"
+    // `./gradlew generateGodotTemplates` build command instead after running the `scons` command(s).
+    // The {selectedAbis} values must be from the {supportedAbis} values.
+    selectedAbis = ["arm64v8"]
 }
 }
 
 
 def rootDir = "../../.."
 def rootDir = "../../.."
 def binDir = "$rootDir/bin/"
 def binDir = "$rootDir/bin/"
 
 
-def getSconsTaskName(String buildType) {
-    return "compileGodotNativeLibs" + buildType.capitalize()
+def getSconsTaskName(String flavor, String buildType, String abi) {
+    return "compileGodotNativeLibs" + flavor.capitalize() + buildType.capitalize() + abi.capitalize()
 }
 }
 
 
 /**
 /**
@@ -54,6 +55,17 @@ task copyDebugBinaryToBin(type: Copy) {
     include('android_debug.apk')
     include('android_debug.apk')
 }
 }
 
 
+/**
+ * Copy the generated 'android_dev.apk' binary template into the Godot bin directory.
+ * Depends on the app build task to ensure the binary is generated prior to copying.
+ */
+task copyDevBinaryToBin(type: Copy) {
+    dependsOn ':app:assembleDev'
+    from('app/build/outputs/apk/dev')
+    into(binDir)
+    include('android_dev.apk')
+}
+
 /**
 /**
  * Copy the generated 'android_release.apk' binary template into the Godot bin directory.
  * Copy the generated 'android_release.apk' binary template into the Godot bin directory.
  * Depends on the app build task to ensure the binary is generated prior to copying.
  * Depends on the app build task to ensure the binary is generated prior to copying.
@@ -70,7 +82,7 @@ task copyReleaseBinaryToBin(type: Copy) {
  * 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 copyDebugAARToAppModule(type: Copy) {
 task copyDebugAARToAppModule(type: Copy) {
-    dependsOn ':lib:assembleDebug'
+    dependsOn ':lib:assembleTemplateDebug'
     from('lib/build/outputs/aar')
     from('lib/build/outputs/aar')
     into('app/libs/debug')
     into('app/libs/debug')
     include('godot-lib.debug.aar')
     include('godot-lib.debug.aar')
@@ -81,18 +93,40 @@ task copyDebugAARToAppModule(type: Copy) {
  * 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 copyDebugAARToBin(type: Copy) {
 task copyDebugAARToBin(type: Copy) {
-    dependsOn ':lib:assembleDebug'
+    dependsOn ':lib:assembleTemplateDebug'
     from('lib/build/outputs/aar')
     from('lib/build/outputs/aar')
     into(binDir)
     into(binDir)
     include('godot-lib.debug.aar')
     include('godot-lib.debug.aar')
 }
 }
 
 
+/**
+ * Copy the Godot android library archive dev file into the app module dev libs directory.
+ * Depends on the library build task to ensure the AAR file is generated prior to copying.
+ */
+task copyDevAARToAppModule(type: Copy) {
+    dependsOn ':lib:assembleTemplateDev'
+    from('lib/build/outputs/aar')
+    into('app/libs/dev')
+    include('godot-lib.dev.aar')
+}
+
+/**
+ * Copy the Godot android library archive dev file into the root bin directory.
+ * Depends on the library build task to ensure the AAR file is generated prior to copying.
+ */
+task copyDevAARToBin(type: Copy) {
+    dependsOn ':lib:assembleTemplateDev'
+    from('lib/build/outputs/aar')
+    into(binDir)
+    include('godot-lib.dev.aar')
+}
+
 /**
 /**
  * Copy the Godot android library archive release file into the app module release libs directory.
  * 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.
  * Depends on the library build task to ensure the AAR file is generated prior to copying.
  */
  */
 task copyReleaseAARToAppModule(type: Copy) {
 task copyReleaseAARToAppModule(type: Copy) {
-    dependsOn ':lib:assembleRelease'
+    dependsOn ':lib:assembleTemplateRelease'
     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')
@@ -103,7 +137,7 @@ task copyReleaseAARToAppModule(type: Copy) {
  * 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 copyReleaseAARToBin(type: Copy) {
 task copyReleaseAARToBin(type: Copy) {
-    dependsOn ':lib:assembleRelease'
+    dependsOn ':lib:assembleTemplateRelease'
     from('lib/build/outputs/aar')
     from('lib/build/outputs/aar')
     into(binDir)
     into(binDir)
     include('godot-lib.release.aar')
     include('godot-lib.release.aar')
@@ -111,7 +145,7 @@ task copyReleaseAARToBin(type: Copy) {
 
 
 /**
 /**
  * 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', 'copyDevAAR' and 'copyReleaseAAR'.
  * The zip file also includes some gradle tools to allow building of the custom build.
  * The zip file also includes some gradle tools to allow building of the custom build.
  */
  */
 task zipCustomBuild(type: Zip) {
 task zipCustomBuild(type: Zip) {
@@ -130,8 +164,18 @@ def templateExcludedBuildTask() {
     def excludedTasks = []
     def excludedTasks = []
     if (!isAndroidStudio()) {
     if (!isAndroidStudio()) {
         logger.lifecycle("Excluding Android studio build tasks")
         logger.lifecycle("Excluding Android studio build tasks")
-        for (String buildType : supportedTargets) {
-            excludedTasks += ":lib:" + getSconsTaskName(buildType)
+        for (String flavor : supportedFlavors) {
+            for (String buildType : supportedTargetsMap.keySet()) {
+                if (buildType == "release" && flavor == "editor") {
+                    // The editor can't be used with target=release as debugging tools are then not
+                    // included, and it would crash on errors instead of reporting them.
+                    continue
+                }
+
+                for (String abi : selectedAbis) {
+                    excludedTasks += ":lib:" + getSconsTaskName(flavor, buildType, abi)
+                }
+            }
         }
         }
     }
     }
     return excludedTasks
     return excludedTasks
@@ -141,7 +185,7 @@ def templateBuildTasks() {
     def tasks = []
     def tasks = []
 
 
     // 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) {
+    for (String target : supportedTargetsMap.keySet()) {
         File targetLibs = new File("lib/libs/" + target)
         File targetLibs = new File("lib/libs/" + target)
         if (targetLibs != null
         if (targetLibs != null
             && targetLibs.isDirectory()
             && targetLibs.isDirectory()
@@ -167,6 +211,50 @@ def isAndroidStudio() {
     return sysProps != null && sysProps['idea.platform.prefix'] != null
     return sysProps != null && sysProps['idea.platform.prefix'] != null
 }
 }
 
 
+task copyEditorDebugBinaryToBin(type: Copy) {
+    dependsOn ':editor:assembleDebug'
+    from('editor/build/outputs/apk/debug')
+    into(binDir)
+    include('android_editor.apk')
+}
+
+task copyEditorDevBinaryToBin(type: Copy) {
+    dependsOn ':editor:assembleDev'
+    from('editor/build/outputs/apk/dev')
+    into(binDir)
+    include('android_editor_dev.apk')
+}
+
+/**
+ * Generate the Godot Editor Android apk.
+ *
+ * Note: The Godot 'tools' shared libraries must have been generated (via scons) prior to running
+ * this gradle task. The task will only build the apk(s) for which the shared libraries is
+ * available.
+ */
+task generateGodotEditor {
+    gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
+
+    def tasks = []
+
+    for (String target : supportedTargetsMap.keySet()) {
+        if (target == "release") {
+            // The editor can't be used with target=release as debugging tools are then not
+            // included, and it would crash on errors instead of reporting them.
+            continue
+        }
+        File targetLibs = new File("lib/libs/tools/" + target)
+        if (targetLibs != null
+            && targetLibs.isDirectory()
+            && targetLibs.listFiles() != null
+            && targetLibs.listFiles().length > 0) {
+            tasks += "copyEditor${target.capitalize()}BinaryToBin"
+        }
+    }
+
+    dependsOn = tasks
+}
+
 /**
 /**
  * Master task used to coordinate the tasks defined above to generate the set of Godot templates.
  * Master task used to coordinate the tasks defined above to generate the set of Godot templates.
  */
  */
@@ -191,7 +279,27 @@ task generateDevTemplate {
 }
 }
 
 
 /**
 /**
- * Clean the generated artifacts.
+ * Clean the generated editor artifacts.
+ */
+task cleanGodotEditor(type: Delete) {
+    // Delete the generated native tools libs
+    delete("lib/libs/tools")
+
+    // Delete the library generated AAR files
+    delete("lib/build/outputs/aar")
+
+    // Delete the generated binary apks
+    delete("editor/build/outputs/apk")
+
+    // Delete the Godot editor apks in the Godot bin directory
+    delete("$binDir/android_editor.apk")
+    delete("$binDir/android_editor_dev.apk")
+
+    finalizedBy getTasksByName("clean", true)
+}
+
+/**
+ * Clean the generated template artifacts.
  */
  */
 task cleanGodotTemplates(type: Delete) {
 task cleanGodotTemplates(type: Delete) {
     // Delete the generated native libs
     // Delete the generated native libs
@@ -208,9 +316,11 @@ task cleanGodotTemplates(type: Delete) {
 
 
     // Delete the Godot templates in the Godot bin directory
     // Delete the Godot templates in the Godot bin directory
     delete("$binDir/android_debug.apk")
     delete("$binDir/android_debug.apk")
+    delete("$binDir/android_dev.apk")
     delete("$binDir/android_release.apk")
     delete("$binDir/android_release.apk")
     delete("$binDir/android_source.zip")
     delete("$binDir/android_source.zip")
     delete("$binDir/godot-lib.debug.aar")
     delete("$binDir/godot-lib.debug.aar")
+    delete("$binDir/godot-lib.dev.aar")
     delete("$binDir/godot-lib.release.aar")
     delete("$binDir/godot-lib.release.aar")
 
 
     finalizedBy getTasksByName("clean", true)
     finalizedBy getTasksByName("clean", true)

+ 74 - 0
platform/android/java/editor/build.gradle

@@ -0,0 +1,74 @@
+// Gradle build config for Godot Engine's Android port.
+apply plugin: 'com.android.application'
+
+dependencies {
+    implementation libraries.kotlinStdLib
+    implementation libraries.androidxFragment
+    implementation project(":lib")
+}
+
+android {
+    compileSdkVersion versions.compileSdk
+    buildToolsVersion versions.buildTools
+    ndkVersion versions.ndkVersion
+
+    defaultConfig {
+        // The 'applicationId' suffix allows to install Godot 3.x(v3) and 4.x(v4) on the same device
+        applicationId "org.godotengine.editor.v3"
+        versionCode getGodotLibraryVersionCode()
+        versionName getGodotLibraryVersionName()
+        minSdkVersion versions.minSdk
+        //noinspection ExpiredTargetSdkVersion - Restrict to version 29 until https://github.com/godotengine/godot/pull/51815 is submitted
+        targetSdkVersion 29 // versions.targetSdk
+
+        missingDimensionStrategy 'products', 'editor'
+    }
+
+    compileOptions {
+        sourceCompatibility versions.javaVersion
+        targetCompatibility versions.javaVersion
+    }
+
+    buildTypes {
+        dev {
+            initWith debug
+            applicationIdSuffix ".dev"
+        }
+
+        debug {
+            initWith release
+
+            // Need to swap with the release signing config when this is ready for public release.
+            signingConfig signingConfigs.debug
+        }
+
+        release {
+            // This buildtype is disabled below.
+            // The editor can't be used with target=release only, as debugging tools are then not
+            // included, and it would crash on errors instead of reporting them.
+        }
+    }
+
+    packagingOptions {
+        // 'doNotStrip' is enabled for development within Android Studio
+        if (shouldNotStrip()) {
+            doNotStrip '**/*.so'
+        }
+    }
+
+    // Disable 'release' buildtype.
+    // The editor can't be used with target=release only, as debugging tools are then not
+    // included, and it would crash on errors instead of reporting them.
+    variantFilter { variant ->
+        if (variant.buildType.name == "release") {
+            setIgnore(true)
+        }
+    }
+
+    applicationVariants.all { variant ->
+        variant.outputs.all { output ->
+            def suffix = variant.name == "dev" ? "_dev" : ""
+            output.outputFileName = "android_editor${suffix}.apk"
+        }
+    }
+}

+ 4 - 0
platform/android/java/editor/src/dev/res/values/strings.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+	<string name="godot_editor_name_string">Godot Editor (dev)</string>
+</resources>

+ 67 - 0
platform/android/java/editor/src/main/AndroidManifest.xml

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="org.godotengine.editor"
+    android:installLocation="auto">
+
+    <supports-screens
+        android:largeScreens="true"
+        android:normalScreens="true"
+        android:smallScreens="true"
+        android:xlargeScreens="true" />
+
+    <uses-feature
+        android:glEsVersion="0x00020000"
+        android:required="true" />
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+
+    <application
+        android:allowBackup="false"
+        android:icon="@mipmap/icon"
+        android:label="@string/godot_editor_name_string"
+        tools:ignore="GoogleAppIndexingWarning"
+        android:requestLegacyExternalStorage="true">
+
+        <activity
+            android:name=".GodotProjectManager"
+            android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
+            android:launchMode="singleTask"
+            android:resizeableActivity="false"
+            android:screenOrientation="landscape"
+            android:exported="true"
+            android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
+            android:process=":GodotProjectManager">
+
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <activity
+            android:name=".GodotEditor"
+            android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
+            android:process=":GodotEditor"
+            android:launchMode="singleTask"
+            android:resizeableActivity="false"
+            android:screenOrientation="landscape"
+            android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
+        </activity>
+
+        <activity
+            android:name=".GodotGame"
+            android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
+            android:label="@string/godot_project_name_string"
+            android:process=":GodotGame"
+            android:launchMode="singleTask"
+            android:resizeableActivity="false"
+            android:screenOrientation="landscape"
+            android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
+        </activity>
+
+    </application>
+
+</manifest>

+ 110 - 0
platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.java

@@ -0,0 +1,110 @@
+/*************************************************************************/
+/*  GodotEditor.java                                                     */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 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.editor;
+
+import org.godotengine.godot.FullScreenGodotApp;
+import org.godotengine.godot.utils.PermissionsUtil;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Debug;
+
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Base class for the Godot Android Editor activities.
+ *
+ * This provides the basic templates for the activities making up this application.
+ * Each derived activity runs in its own process, which enable up to have several instances of
+ * the Godot engine up and running at the same time.
+ *
+ * It also plays the role of the primary editor window.
+ */
+public class GodotEditor extends FullScreenGodotApp {
+	private static final boolean WAIT_FOR_DEBUGGER = false;
+	private static final String COMMAND_LINE_PARAMS = "command_line_params";
+
+	private static final String EDITOR_ARG = "--editor";
+	private static final String PROJECT_MANAGER_ARG = "--project-manager";
+
+	private final List<String> commandLineParams = new ArrayList<>();
+
+	@Override
+	public void onCreate(Bundle savedInstanceState) {
+		PermissionsUtil.requestManifestPermissions(this);
+
+		String[] params = getIntent().getStringArrayExtra(COMMAND_LINE_PARAMS);
+		updateCommandLineParams(params);
+
+		if (BuildConfig.BUILD_TYPE.equals("debug") && WAIT_FOR_DEBUGGER) {
+			Debug.waitForDebugger();
+		}
+		super.onCreate(savedInstanceState);
+	}
+
+	private void updateCommandLineParams(@Nullable String[] args) {
+		// Update the list of command line params with the new args
+		commandLineParams.clear();
+		if (args != null && args.length > 0) {
+			commandLineParams.addAll(Arrays.asList(args));
+		}
+	}
+
+	@Override
+	public List<String> getCommandLine() {
+		return commandLineParams;
+	}
+
+	@Override
+	public void onNewGodotInstanceRequested(String[] args) {
+		// Parse the arguments to figure out which activity to start.
+		Class<?> targetClass = GodotGame.class;
+		for (String arg : args) {
+			if (EDITOR_ARG.equals(arg)) {
+				targetClass = GodotEditor.class;
+				break;
+			}
+
+			if (PROJECT_MANAGER_ARG.equals(arg)) {
+				targetClass = GodotProjectManager.class;
+				break;
+			}
+		}
+
+		// Launch a new activity
+		Intent newInstance = new Intent(this, targetClass).putExtra(COMMAND_LINE_PARAMS, args);
+		startActivity(newInstance);
+	}
+}

+ 37 - 0
platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.java

@@ -0,0 +1,37 @@
+/*************************************************************************/
+/*  GodotGame.java                                                       */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 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.editor;
+
+/**
+ * Drives the 'run project' window of the Godot Editor.
+ */
+public class GodotGame extends GodotEditor {
+}

+ 41 - 0
platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.java

@@ -0,0 +1,41 @@
+/*************************************************************************/
+/*  GodotProjectManager.java                                             */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 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.editor;
+
+/**
+ * Launcher activity for the Godot Android Editor.
+ *
+ * It presents the user with the project manager interface.
+ * Upon selection of a project, this activity (via its parent logic) starts the
+ * {@link GodotEditor} activity.
+ */
+public class GodotProjectManager extends GodotEditor {
+}

+ 4 - 0
platform/android/java/editor/src/main/res/values/strings.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+	<string name="godot_editor_name_string">Godot Editor</string>
+</resources>

+ 62 - 19
platform/android/java/lib/build.gradle

@@ -18,14 +18,13 @@ def pathToRootDir = "../../../../"
 android {
 android {
     compileSdkVersion versions.compileSdk
     compileSdkVersion versions.compileSdk
     buildToolsVersion versions.buildTools
     buildToolsVersion versions.buildTools
-
     ndkVersion versions.ndkVersion
     ndkVersion versions.ndkVersion
 
 
     defaultConfig {
     defaultConfig {
         minSdkVersion versions.minSdk
         minSdkVersion versions.minSdk
         targetSdkVersion versions.targetSdk
         targetSdkVersion versions.targetSdk
 
 
-        manifestPlaceholders = [godotLibraryVersion: getGodotLibraryVersion()]
+        manifestPlaceholders = [godotLibraryVersion: getGodotLibraryVersionName()]
     }
     }
 
 
     namespace = "org.godotengine.godot"
     namespace = "org.godotengine.godot"
@@ -35,6 +34,18 @@ android {
         targetCompatibility versions.javaVersion
         targetCompatibility versions.javaVersion
     }
     }
 
 
+    buildTypes {
+        dev {
+            initWith debug
+        }
+    }
+
+    flavorDimensions "products"
+    productFlavors {
+        editor {}
+        template {}
+    }
+
     lintOptions {
     lintOptions {
         abortOnError false
         abortOnError false
         disable 'MissingTranslation', 'UnusedResources'
         disable 'MissingTranslation', 'UnusedResources'
@@ -58,24 +69,50 @@ android {
             aidl.srcDirs = ['aidl']
             aidl.srcDirs = ['aidl']
             assets.srcDirs = ['assets']
             assets.srcDirs = ['assets']
         }
         }
+
         debug.jniLibs.srcDirs = ['libs/debug']
         debug.jniLibs.srcDirs = ['libs/debug']
+        dev.jniLibs.srcDirs = ['libs/dev']
         release.jniLibs.srcDirs = ['libs/release']
         release.jniLibs.srcDirs = ['libs/release']
+
+        // Editor jni library
+        editorDebug.jniLibs.srcDirs = ['libs/tools/debug']
+        editorDev.jniLibs.srcDirs = ['libs/tools/dev']
+    }
+
+    // Disable 'editorRelease'.
+    // The editor can't be used with target=release as debugging tools are then not
+    // included, and it would crash on errors instead of reporting them.
+    variantFilter { variant ->
+        if (variant.name == "editorRelease") {
+            setIgnore(true)
+        }
     }
     }
 
 
     libraryVariants.all { variant ->
     libraryVariants.all { variant ->
-        variant.outputs.all { output ->
-            output.outputFileName = "godot-lib.${variant.name}.aar"
+        def flavorName = variant.getFlavorName()
+        if (flavorName == null || flavorName == "") {
+            throw new GradleException("Invalid product flavor: $flavorName")
         }
         }
 
 
-        def buildType = variant.buildType.name.capitalize()
+        boolean toolsFlag = flavorName == "editor"
+
+        def buildType = variant.buildType.name
+        if (buildType == null || buildType == "" || !supportedTargetsMap.containsKey(buildType)) {
+            throw new GradleException("Invalid build type: $buildType")
+        }
 
 
-        def releaseTarget = buildType.toLowerCase()
-        if (releaseTarget == null || releaseTarget == "") {
-            throw new GradleException("Invalid build type: " + buildType)
+        def sconsTarget = supportedTargetsMap[buildType]
+        if (sconsTarget == null || sconsTarget == "") {
+            throw new GradleException("Invalid scons target: $sconsTarget")
         }
         }
 
 
-        if (!supportedAbis.contains(defaultAbi)) {
-            throw new GradleException("Invalid default abi: " + defaultAbi)
+        // Update the name of the generated library
+        def outputSuffix = "${buildType}.aar"
+        if (toolsFlag) {
+            outputSuffix = "tools.$outputSuffix"
+        }
+        variant.outputs.all { output ->
+            output.outputFileName = "godot-lib.${outputSuffix}"
         }
         }
 
 
         // Find scons' executable path
         // Find scons' executable path
@@ -110,20 +147,26 @@ android {
             logger.lifecycle("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}")
             logger.lifecycle("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}")
         }
         }
 
 
-        // Creating gradle task to generate the native libraries for the default abi.
-        def taskName = getSconsTaskName(buildType)
-        tasks.create(name: taskName, type: Exec) {
-            executable sconsExecutableFile.absolutePath
-            args "--directory=${pathToRootDir}", "platform=android", "target=${releaseTarget}", "android_arch=${defaultAbi}", "-j" + Runtime.runtime.availableProcessors()
-        }
+        for (String selectedAbi : selectedAbis) {
+            if (!supportedAbis.contains(selectedAbi)) {
+                throw new GradleException("Invalid selected abi: $selectedAbi")
+            }
 
 
-        // Schedule the tasks so the generated libs are present before the aar file is packaged.
-        tasks["merge${buildType}JniLibFolders"].dependsOn taskName
+            // Creating gradle task to generate the native libraries for the selected abi.
+            def taskName = getSconsTaskName(flavorName, buildType, selectedAbi)
+            tasks.create(name: taskName, type: Exec) {
+                executable sconsExecutableFile.absolutePath
+                args "--directory=${pathToRootDir}", "platform=android", "tools=${toolsFlag}", "target=${sconsTarget}", "android_arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors()
+            }
+
+            // Schedule the tasks so the generated libs are present before the aar file is packaged.
+            tasks["merge${flavorName.capitalize()}${buildType.capitalize()}JniLibFolders"].dependsOn taskName
+        }
     }
     }
 
 
     // TODO: Enable when issues with AGP 7.1+ are resolved (https://github.com/GodotVR/godot_openxr/issues/187).
     // TODO: Enable when issues with AGP 7.1+ are resolved (https://github.com/GodotVR/godot_openxr/issues/187).
 //    publishing {
 //    publishing {
-//        singleVariant("release") {
+//        singleVariant("templateRelease") {
 //            withSourcesJar()
 //            withSourcesJar()
 //            withJavadocJar()
 //            withJavadocJar()
 //        }
 //        }

+ 20 - 7
platform/android/java/lib/src/org/godotengine/godot/Godot.java

@@ -47,7 +47,6 @@ import android.app.AlertDialog;
 import android.app.PendingIntent;
 import android.app.PendingIntent;
 import android.content.ClipData;
 import android.content.ClipData;
 import android.content.ClipboardManager;
 import android.content.ClipboardManager;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Context;
 import android.content.Intent;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences;
@@ -449,9 +448,11 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 	}
 	}
 
 
 	public void restart() {
 	public void restart() {
-		if (godotHost != null) {
-			godotHost.onGodotRestartRequested(this);
-		}
+		runOnUiThread(() -> {
+			if (godotHost != null) {
+				godotHost.onGodotRestartRequested(this);
+			}
+		});
 	}
 	}
 
 
 	public void alert(final String message, final String title) {
 	public void alert(final String message, final String title) {
@@ -995,9 +996,11 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 	private void forceQuit() {
 	private void forceQuit() {
 		// TODO: This is a temp solution. The proper fix will involve tracking down and properly shutting down each
 		// TODO: This is a temp solution. The proper fix will involve tracking down and properly shutting down each
 		// native Godot components that is started in Godot#onVideoInit.
 		// native Godot components that is started in Godot#onVideoInit.
-		if (godotHost != null) {
-			godotHost.onGodotForceQuit(this);
-		}
+		runOnUiThread(() -> {
+			if (godotHost != null) {
+				godotHost.onGodotForceQuit(this);
+			}
+		});
 	}
 	}
 
 
 	private boolean obbIsCorrupted(String f, String main_pack_md5) {
 	private boolean obbIsCorrupted(String f, String main_pack_md5) {
@@ -1146,7 +1149,17 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 		mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress,
 		mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress,
 				progress.mOverallTotal));
 				progress.mOverallTotal));
 	}
 	}
+
 	public void initInputDevices() {
 	public void initInputDevices() {
 		mView.initInputDevices();
 		mView.initInputDevices();
 	}
 	}
+
+	@Keep
+	private void createNewGodotInstance(String[] args) {
+		runOnUiThread(() -> {
+			if (godotHost != null) {
+				godotHost.onNewGodotInstanceRequested(args);
+			}
+		});
+	}
 }
 }

+ 9 - 1
platform/android/java/lib/src/org/godotengine/godot/GodotHost.java

@@ -60,8 +60,16 @@ public interface GodotHost {
 	default void onGodotForceQuit(Godot instance) {}
 	default void onGodotForceQuit(Godot instance) {}
 
 
 	/**
 	/**
-	 * Invoked on the GL thread when the Godot instance wants to be restarted. It's up to the host
+	 * Invoked on the UI thread when the Godot instance wants to be restarted. It's up to the host
 	 * to perform the appropriate action(s).
 	 * to perform the appropriate action(s).
 	 */
 	 */
 	default void onGodotRestartRequested(Godot instance) {}
 	default void onGodotRestartRequested(Godot instance) {}
+
+	/**
+	 * Invoked on the UI thread when a new Godot instance is requested. It's up to the host to
+	 * perform the appropriate action(s).
+	 *
+	 * @param args Arguments used to initialize the new instance.
+	 */
+	default void onNewGodotInstanceRequested(String[] args) {}
 }
 }

+ 2 - 0
platform/android/java/nativeSrcsConfigs/CMakeLists.txt

@@ -17,3 +17,5 @@ target_include_directories(${PROJECT_NAME}
         SYSTEM PUBLIC
         SYSTEM PUBLIC
         ${GODOT_ROOT_DIR}
         ${GODOT_ROOT_DIR}
         ${GODOT_ROOT_DIR}/modules/gdnative/include)
         ${GODOT_ROOT_DIR}/modules/gdnative/include)
+
+add_definitions(-DUNIX_ENABLED -DTOOLS_ENABLED)

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

@@ -6,6 +6,7 @@ plugins {
 android {
 android {
     compileSdkVersion versions.compileSdk
     compileSdkVersion versions.compileSdk
     buildToolsVersion versions.buildTools
     buildToolsVersion versions.buildTools
+    ndkVersion versions.ndkVersion
 
 
     defaultConfig {
     defaultConfig {
         minSdkVersion versions.minSdk
         minSdkVersion versions.minSdk

+ 3 - 8
platform/android/java/scripts/publish-module.gradle

@@ -7,20 +7,15 @@ version = PUBLISH_VERSION
 afterEvaluate {
 afterEvaluate {
     publishing {
     publishing {
         publications {
         publications {
-            release(MavenPublication) {
+            templateRelease(MavenPublication) {
+                from components.templateRelease
+
                 // The coordinates of the library, being set from variables that
                 // The coordinates of the library, being set from variables that
                 // we'll set up later
                 // we'll set up later
                 groupId ossrhGroupId
                 groupId ossrhGroupId
                 artifactId PUBLISH_ARTIFACT_ID
                 artifactId PUBLISH_ARTIFACT_ID
                 version PUBLISH_VERSION
                 version PUBLISH_VERSION
 
 
-                // Two artifacts, the `aar` (or `jar`) and the sources
-                if (project.plugins.findPlugin("com.android.library")) {
-                    from components.release
-                } else {
-                    from components.java
-                }
-
                 // Mostly self-explanatory metadata
                 // Mostly self-explanatory metadata
                 pom {
                 pom {
                     name = PUBLISH_ARTIFACT_ID
                     name = PUBLISH_ARTIFACT_ID

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

@@ -4,6 +4,7 @@ rootProject.name = "Godot"
 include ':app'
 include ':app'
 include ':lib'
 include ':lib'
 include ':nativeSrcsConfigs'
 include ':nativeSrcsConfigs'
+include ':editor'
 
 
 include ':assetPacks:installTime'
 include ':assetPacks:installTime'
 project(':assetPacks:installTime').projectDir = file("app/assetPacks/installTime")
 project(':assetPacks:installTime').projectDir = file("app/assetPacks/installTime")

+ 5 - 1
platform/android/java_godot_lib_jni.cpp

@@ -148,6 +148,9 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *en
 
 
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz) {
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz) {
 	// lets cleanup
 	// lets cleanup
+	if (java_class_wrapper) {
+		memdelete(java_class_wrapper);
+	}
 	if (godot_io_java) {
 	if (godot_io_java) {
 		delete godot_io_java;
 		delete godot_io_java;
 	}
 	}
@@ -159,6 +162,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env
 		delete input_handler;
 		delete input_handler;
 	}
 	}
 	if (os_android) {
 	if (os_android) {
+		os_android->main_loop_end();
 		delete os_android;
 		delete os_android;
 	}
 	}
 }
 }
@@ -188,7 +192,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jc
 		}
 		}
 	}
 	}
 
 
-	Error err = Main::setup("apk", cmdlen, (char **)cmdline, false);
+	Error err = Main::setup(OS_Android::ANDROID_EXEC_PATH, cmdlen, (char **)cmdline, false);
 	if (cmdline) {
 	if (cmdline) {
 		if (j_cmdline) {
 		if (j_cmdline) {
 			for (int i = 0; i < cmdlen; ++i) {
 			for (int i = 0; i < cmdlen; ++i) {

+ 14 - 0
platform/android/java_godot_wrapper.cpp

@@ -80,6 +80,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
 	_get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;");
 	_get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;");
 	_on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V");
 	_on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V");
 	_on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V");
 	_on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V");
+	_create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)V");
 
 
 	// get some Activity method pointers...
 	// get some Activity method pointers...
 	_get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;");
 	_get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;");
@@ -369,3 +370,16 @@ void GodotJavaWrapper::vibrate(int p_duration_ms) {
 		env->CallVoidMethod(godot_instance, _vibrate, p_duration_ms);
 		env->CallVoidMethod(godot_instance, _vibrate, p_duration_ms);
 	}
 	}
 }
 }
+
+void GodotJavaWrapper::create_new_godot_instance(List<String> args) {
+	if (_create_new_godot_instance) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND(env == nullptr);
+
+		jobjectArray jargs = env->NewObjectArray(args.size(), env->FindClass("java/lang/String"), env->NewStringUTF(""));
+		for (int i = 0; i < args.size(); i++) {
+			env->SetObjectArrayElement(jargs, i, env->NewStringUTF(args[i].utf8().get_data()));
+		}
+		env->CallVoidMethod(godot_instance, _create_new_godot_instance, jargs);
+	}
+}

+ 3 - 0
platform/android/java_godot_wrapper.h

@@ -37,6 +37,7 @@
 #include <android/log.h>
 #include <android/log.h>
 #include <jni.h>
 #include <jni.h>
 
 
+#include "core/list.h"
 #include "string_android.h"
 #include "string_android.h"
 
 
 // Class that makes functions in java/src/org/godotengine/godot/Godot.java callable from C++
 // Class that makes functions in java/src/org/godotengine/godot/Godot.java callable from C++
@@ -70,6 +71,7 @@ private:
 	jmethodID _on_godot_setup_completed = 0;
 	jmethodID _on_godot_setup_completed = 0;
 	jmethodID _on_godot_main_loop_started = 0;
 	jmethodID _on_godot_main_loop_started = 0;
 	jmethodID _get_class_loader = 0;
 	jmethodID _get_class_loader = 0;
+	jmethodID _create_new_godot_instance = 0;
 
 
 public:
 public:
 	GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance);
 	GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance);
@@ -106,6 +108,7 @@ public:
 	bool is_activity_resumed();
 	bool is_activity_resumed();
 	void vibrate(int p_duration_ms);
 	void vibrate(int p_duration_ms);
 	String get_input_fallback_mapping();
 	String get_input_fallback_mapping();
+	void create_new_godot_instance(List<String> args);
 };
 };
 
 
 #endif /* !JAVA_GODOT_WRAPPER_H */
 #endif /* !JAVA_GODOT_WRAPPER_H */

+ 57 - 1
platform/android/os_android.cpp

@@ -36,6 +36,7 @@
 #include "drivers/unix/dir_access_unix.h"
 #include "drivers/unix/dir_access_unix.h"
 #include "drivers/unix/file_access_unix.h"
 #include "drivers/unix/file_access_unix.h"
 #include "main/main.h"
 #include "main/main.h"
+#include "scene/main/scene_tree.h"
 #include "servers/visual/visual_server_raster.h"
 #include "servers/visual/visual_server_raster.h"
 #include "servers/visual/visual_server_wrap_mt.h"
 #include "servers/visual/visual_server_wrap_mt.h"
 
 
@@ -50,6 +51,8 @@
 #include "java_godot_io_wrapper.h"
 #include "java_godot_io_wrapper.h"
 #include "java_godot_wrapper.h"
 #include "java_godot_wrapper.h"
 
 
+const char *OS_Android::ANDROID_EXEC_PATH = "apk";
+
 String _remove_symlink(const String &dir) {
 String _remove_symlink(const String &dir) {
 	// Workaround for Android 6.0+ using a symlink.
 	// Workaround for Android 6.0+ using a symlink.
 	// Save the current directory.
 	// Save the current directory.
@@ -100,17 +103,27 @@ const char *OS_Android::get_audio_driver_name(int p_driver) const {
 void OS_Android::initialize_core() {
 void OS_Android::initialize_core() {
 	OS_Unix::initialize_core();
 	OS_Unix::initialize_core();
 
 
+#ifdef TOOLS_ENABLED
+	FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_RESOURCES);
+#else
 	if (use_apk_expansion)
 	if (use_apk_expansion)
 		FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_RESOURCES);
 		FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_RESOURCES);
 	else {
 	else {
 		FileAccess::make_default<FileAccessAndroid>(FileAccess::ACCESS_RESOURCES);
 		FileAccess::make_default<FileAccessAndroid>(FileAccess::ACCESS_RESOURCES);
 	}
 	}
+#endif
 	FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_USERDATA);
 	FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_USERDATA);
 	FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_FILESYSTEM);
 	FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_FILESYSTEM);
+
+#ifdef TOOLS_ENABLED
+	DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_RESOURCES);
+#else
 	if (use_apk_expansion)
 	if (use_apk_expansion)
 		DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_RESOURCES);
 		DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_RESOURCES);
 	else
 	else
 		DirAccess::make_default<DirAccessJAndroid>(DirAccess::ACCESS_RESOURCES);
 		DirAccess::make_default<DirAccessJAndroid>(DirAccess::ACCESS_RESOURCES);
+#endif
+
 	DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_USERDATA);
 	DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_USERDATA);
 	DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_FILESYSTEM);
 	DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_FILESYSTEM);
 
 
@@ -279,6 +292,15 @@ void OS_Android::set_keep_screen_on(bool p_enabled) {
 	godot_java->set_keep_screen_on(p_enabled);
 	godot_java->set_keep_screen_on(p_enabled);
 }
 }
 
 
+void OS_Android::set_low_processor_usage_mode(bool p_enabled) {
+#ifdef TOOLS_ENABLED
+	// Disabled as it causes flickers. We also expect the devices running Godot in editor mode to be high end.
+	OS_Unix::set_low_processor_usage_mode(false);
+#else
+	OS_Unix::set_low_processor_usage_mode(p_enabled);
+#endif
+}
+
 Size2 OS_Android::get_window_size() const {
 Size2 OS_Android::get_window_size() const {
 	return Vector2(default_videomode.width, default_videomode.height);
 	return Vector2(default_videomode.width, default_videomode.height);
 }
 }
@@ -313,8 +335,13 @@ bool OS_Android::main_loop_iterate() {
 }
 }
 
 
 void OS_Android::main_loop_end() {
 void OS_Android::main_loop_end() {
-	if (main_loop)
+	if (main_loop) {
+		SceneTree *scene_tree = Object::cast_to<SceneTree>(main_loop);
+		if (scene_tree) {
+			scene_tree->quit();
+		}
 		main_loop->finish();
 		main_loop->finish();
+	}
 }
 }
 
 
 void OS_Android::main_loop_focusout() {
 void OS_Android::main_loop_focusout() {
@@ -393,7 +420,11 @@ Error OS_Android::shell_open(String p_uri) {
 }
 }
 
 
 String OS_Android::get_resource_dir() const {
 String OS_Android::get_resource_dir() const {
+#ifdef TOOLS_ENABLED
+	return OS_Unix::get_resource_dir();
+#else
 	return "/"; //android has its own filesystem for resources inside the APK
 	return "/"; //android has its own filesystem for resources inside the APK
+#endif
 }
 }
 
 
 String OS_Android::get_locale() const {
 String OS_Android::get_locale() const {
@@ -452,6 +483,14 @@ String OS_Android::get_data_path() const {
 	return get_user_data_dir();
 	return get_user_data_dir();
 }
 }
 
 
+String OS_Android::get_executable_path() const {
+	// Since unix process creation is restricted on Android, we bypass
+	// OS_Unix::get_executable_path() so we can return ANDROID_EXEC_PATH.
+	// Detection of ANDROID_EXEC_PATH allows to handle process creation in an Android compliant
+	// manner.
+	return OS::get_executable_path();
+}
+
 String OS_Android::get_user_data_dir() const {
 String OS_Android::get_user_data_dir() const {
 	if (data_dir_cache != String())
 	if (data_dir_cache != String())
 		return data_dir_cache;
 		return data_dir_cache;
@@ -524,6 +563,10 @@ void OS_Android::vibrate_handheld(int p_duration_ms) {
 	godot_java->vibrate(p_duration_ms);
 	godot_java->vibrate(p_duration_ms);
 }
 }
 
 
+String OS_Android::get_config_path() const {
+	return get_user_data_dir().plus_file("config");
+}
+
 bool OS_Android::_check_internal_feature_support(const String &p_feature) {
 bool OS_Android::_check_internal_feature_support(const String &p_feature) {
 	if (p_feature == "mobile") {
 	if (p_feature == "mobile") {
 		//TODO support etc2 only if GLES3 driver is selected
 		//TODO support etc2 only if GLES3 driver is selected
@@ -567,5 +610,18 @@ OS_Android::OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_god
 	AudioDriverManager::add_driver(&audio_driver_android);
 	AudioDriverManager::add_driver(&audio_driver_android);
 }
 }
 
 
+Error OS_Android::execute(const String &p_path, const List<String> &p_arguments, bool p_blocking, ProcessID *r_child_id, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex, bool p_open_console) {
+	if (p_path == ANDROID_EXEC_PATH) {
+		return create_instance(p_arguments, r_child_id);
+	} else {
+		return OS_Unix::execute(p_path, p_arguments, p_blocking, r_child_id, r_pipe, r_exitcode, read_stderr, p_pipe_mutex, p_open_console);
+	}
+}
+
+Error OS_Android::create_instance(const List<String> &p_arguments, ProcessID *r_child_id) {
+	godot_java->create_new_godot_instance(p_arguments);
+	return OK;
+}
+
 OS_Android::~OS_Android() {
 OS_Android::~OS_Android() {
 }
 }

+ 11 - 0
platform/android/os_android.h

@@ -70,6 +70,8 @@ class OS_Android : public OS_Unix {
 	bool transparency_enabled = false;
 	bool transparency_enabled = false;
 
 
 public:
 public:
+	static const char *ANDROID_EXEC_PATH;
+
 	// functions used by main to initialize/deinitialize the OS
 	// functions used by main to initialize/deinitialize the OS
 	virtual int get_video_driver_count() const;
 	virtual int get_video_driver_count() const;
 	virtual const char *get_video_driver_name(int p_driver) const;
 	virtual const char *get_video_driver_name(int p_driver) const;
@@ -111,6 +113,7 @@ public:
 	virtual void get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen = 0) const;
 	virtual void get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen = 0) const;
 
 
 	virtual void set_keep_screen_on(bool p_enabled);
 	virtual void set_keep_screen_on(bool p_enabled);
+	virtual void set_low_processor_usage_mode(bool p_enabled);
 
 
 	virtual Size2 get_window_size() const;
 	virtual Size2 get_window_size() const;
 	virtual Rect2 get_window_safe_area() const;
 	virtual Rect2 get_window_safe_area() const;
@@ -144,6 +147,7 @@ public:
 	virtual ScreenOrientation get_screen_orientation() const;
 	virtual ScreenOrientation get_screen_orientation() const;
 
 
 	virtual Error shell_open(String p_uri);
 	virtual Error shell_open(String p_uri);
+	virtual String get_executable_path() const;
 	virtual String get_user_data_dir() const;
 	virtual String get_user_data_dir() const;
 	virtual String get_data_path() const;
 	virtual String get_data_path() const;
 	virtual String get_cache_path() const;
 	virtual String get_cache_path() const;
@@ -173,9 +177,16 @@ public:
 	virtual String get_joy_guid(int p_device) const;
 	virtual String get_joy_guid(int p_device) const;
 	void vibrate_handheld(int p_duration_ms);
 	void vibrate_handheld(int p_duration_ms);
 
 
+	virtual String get_config_path() const;
+
+	virtual Error execute(const String &p_path, const List<String> &p_arguments, bool p_blocking = true, ProcessID *r_child_id = nullptr, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false);
+
 	virtual bool _check_internal_feature_support(const String &p_feature);
 	virtual bool _check_internal_feature_support(const String &p_feature);
 	OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_godot_io_java, bool p_use_apk_expansion);
 	OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_godot_io_java, bool p_use_apk_expansion);
 	~OS_Android();
 	~OS_Android();
+
+private:
+	Error create_instance(const List<String> &p_arguments, ProcessID *r_child_id);
 };
 };
 
 
 #endif
 #endif

+ 3 - 0
scene/main/viewport.cpp

@@ -1388,6 +1388,9 @@ Vector2 Viewport::_get_window_offset() const {
 }
 }
 
 
 Ref<InputEvent> Viewport::_make_input_local(const Ref<InputEvent> &ev) {
 Ref<InputEvent> Viewport::_make_input_local(const Ref<InputEvent> &ev) {
+	if (ev.is_null()) {
+		return ev;
+	}
 	Vector2 vp_ofs = _get_window_offset();
 	Vector2 vp_ofs = _get_window_offset();
 	Transform2D ai = get_final_transform().affine_inverse() * _get_input_pre_xform();
 	Transform2D ai = get_final_transform().affine_inverse() * _get_input_pre_xform();