Add 'engine/' from commit 'a8e37fc010'
git-subtree-dir: engine git-subtree-mainline:b74841629egit-subtree-split:a8e37fc010
This commit is contained in:
commit
c3f9669b10
14113 changed files with 7458101 additions and 0 deletions
41
engine/platform/android/java/THIRDPARTY.md
Normal file
41
engine/platform/android/java/THIRDPARTY.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Third-party libraries
|
||||
|
||||
This file list third-party libraries used in the Android source folder,
|
||||
with their provenance and, when relevant, modifications made to those files.
|
||||
|
||||
## com.google.android.vending.expansion.downloader
|
||||
|
||||
- Upstream: https://github.com/google/play-apk-expansion/tree/master/apkx_library
|
||||
- Version: git (9ecf54e, 2017)
|
||||
- License: Apache 2.0
|
||||
|
||||
Overwrite all files under:
|
||||
|
||||
- `lib/src/com/google/android/vending/expansion/downloader`
|
||||
|
||||
Some files have been modified for yet unclear reasons.
|
||||
See the `lib/patches/com.google.android.vending.expansion.downloader.patch` file.
|
||||
|
||||
## com.google.android.vending.licensing
|
||||
|
||||
- Upstream: https://github.com/google/play-licensing/tree/master/lvl_library/
|
||||
- Version: git (eb57657, 2018) with modifications
|
||||
- License: Apache 2.0
|
||||
|
||||
Overwrite all files under:
|
||||
|
||||
- `lib/aidl/com/android/vending/licensing`
|
||||
- `lib/src/com/google/android/vending/licensing`
|
||||
|
||||
Some files have been modified to silence linter errors or fix downstream issues.
|
||||
See the `lib/patches/com.google.android.vending.licensing.patch` file.
|
||||
|
||||
## com.android.apksig
|
||||
|
||||
- Upstream: https://android.googlesource.com/platform/tools/apksig/+/ac5cbb07d87cc342fcf07715857a812305d69888
|
||||
- Version: git (ac5cbb07d87cc342fcf07715857a812305d69888, 2024)
|
||||
- License: Apache 2.0
|
||||
|
||||
Overwrite all files under:
|
||||
|
||||
- `editor/src/main/java/com/android/apksig`
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
plugins {
|
||||
id 'com.android.asset-pack'
|
||||
}
|
||||
|
||||
assetPack {
|
||||
packName = "assetPackInstallTime" // Directory name for the asset pack
|
||||
dynamicDelivery {
|
||||
deliveryType = "install-time" // Delivery mode
|
||||
}
|
||||
}
|
||||
337
engine/platform/android/java/app/build.gradle
Normal file
337
engine/platform/android/java/app/build.gradle
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
// Gradle build config for Godot Engine's Android port.
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
apply from: 'config.gradle'
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
maven { url "https://central.sonatype.com/repository/maven-snapshots/"}
|
||||
|
||||
// Godot user plugins custom maven repos
|
||||
String[] mavenRepos = getGodotPluginsMavenRepos()
|
||||
if (mavenRepos != null && mavenRepos.size() > 0) {
|
||||
for (String repoUrl : mavenRepos) {
|
||||
maven {
|
||||
url repoUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
// Initializes a placeholder for the monoImplementation dependency configuration.
|
||||
monoImplementation {}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Android instrumented test dependencies
|
||||
androidTestImplementation "androidx.test.ext:junit:$versions.junitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$versions.espressoCoreVersion"
|
||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$versions.kotlinTestVersion"
|
||||
androidTestImplementation "androidx.test:runner:$versions.testRunnerVersion"
|
||||
androidTestUtil "androidx.test:orchestrator:$versions.testOrchestratorVersion"
|
||||
|
||||
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
|
||||
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
|
||||
implementation "androidx.documentfile:documentfile:$versions.documentfileVersion"
|
||||
|
||||
if (rootProject.findProject(":lib")) {
|
||||
implementation project(":lib")
|
||||
} else if (rootProject.findProject(":godot:lib")) {
|
||||
implementation project(":godot:lib")
|
||||
} else {
|
||||
// Godot gradle 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.
|
||||
debugImplementation fileTree(dir: 'libs/debug', include: ['**/*.jar', '*.aar'])
|
||||
releaseImplementation fileTree(dir: 'libs/release', include: ['**/*.jar', '*.aar'])
|
||||
}
|
||||
|
||||
// Godot user plugins remote dependencies
|
||||
String[] remoteDeps = getGodotPluginsRemoteBinaries()
|
||||
if (remoteDeps != null && remoteDeps.size() > 0) {
|
||||
def platformPattern = /^\s*(platform|enforcedPlatform)\s*\(\s*['"]*(\S+)['"]*\s*\)$/
|
||||
for (String dep : remoteDeps) {
|
||||
def matcher = dep =~ platformPattern
|
||||
if (matcher) {
|
||||
switch (matcher[0][1]) {
|
||||
case "platform":
|
||||
implementation platform(matcher[0][2])
|
||||
break
|
||||
|
||||
case "enforcedPlatform":
|
||||
implementation enforcedPlatform(matcher[0][2])
|
||||
break
|
||||
|
||||
default:
|
||||
throw new GradleException("Invalid remote platform dependency: $dep")
|
||||
break
|
||||
}
|
||||
} else {
|
||||
implementation dep
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Godot user plugins local dependencies
|
||||
String[] pluginsBinaries = getGodotPluginsLocalBinaries()
|
||||
if (pluginsBinaries != null && pluginsBinaries.size() > 0) {
|
||||
implementation files(pluginsBinaries)
|
||||
}
|
||||
|
||||
// Automatically pick up local dependencies in res://addons
|
||||
String addonsDirectory = getAddonsDirectory()
|
||||
if (addonsDirectory != null && !addonsDirectory.isBlank()) {
|
||||
implementation fileTree(dir: "$addonsDirectory", include: ['*.jar', '*.aar'])
|
||||
}
|
||||
|
||||
// .NET dependencies
|
||||
String jar = '../../../../modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar'
|
||||
if (file(jar).exists()) {
|
||||
monoImplementation files(jar)
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
buildToolsVersion versions.buildTools
|
||||
ndkVersion versions.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility versions.javaVersion
|
||||
targetCompatibility versions.javaVersion
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = versions.javaVersion
|
||||
}
|
||||
|
||||
assetPacks = [":assetPackInstallTime"]
|
||||
|
||||
namespace = 'com.godot.game'
|
||||
|
||||
defaultConfig {
|
||||
// The default ignore pattern for the 'assets' directory includes hidden files and directories which are used by Godot projects.
|
||||
aaptOptions {
|
||||
ignoreAssetsPattern "!.svn:!.git:!.gitignore:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~"
|
||||
}
|
||||
|
||||
ndk {
|
||||
debugSymbolLevel 'NONE'
|
||||
String[] export_abi_list = getExportEnabledABIs()
|
||||
abiFilters export_abi_list
|
||||
}
|
||||
|
||||
// Feel free to modify the application id to your own.
|
||||
applicationId getExportPackageName()
|
||||
versionCode getExportVersionCode()
|
||||
versionName getExportVersionName()
|
||||
minSdkVersion getExportMinSdkVersion()
|
||||
targetSdkVersion getExportTargetSdkVersion()
|
||||
|
||||
missingDimensionStrategy 'products', 'template'
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// The following argument makes the Android Test Orchestrator run its
|
||||
// "pm clear" command after each test invocation. This command ensures
|
||||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
disable 'MissingTranslation', 'UnusedResources'
|
||||
}
|
||||
|
||||
ndkVersion versions.ndkVersion
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
|
||||
// Debug symbols are kept for development within Android Studio.
|
||||
if (shouldNotStrip()) {
|
||||
jniLibs {
|
||||
keepDebugSymbols += '**/*.so'
|
||||
}
|
||||
}
|
||||
|
||||
jniLibs {
|
||||
// Setting this to true causes AGP to package compressed native libraries when building the app
|
||||
// For more background, see:
|
||||
// - https://developer.android.com/build/releases/past-releases/agp-3-6-0-release-notes#extractNativeLibs
|
||||
// - https://stackoverflow.com/a/44704840
|
||||
useLegacyPackaging shouldUseLegacyPackaging()
|
||||
}
|
||||
|
||||
// Always select Godot's version of libc++_shared.so in case deps have their own
|
||||
pickFirst 'lib/x86/libc++_shared.so'
|
||||
pickFirst 'lib/x86_64/libc++_shared.so'
|
||||
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
|
||||
pickFirst 'lib/arm64-v8a/libc++_shared.so'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
debug {
|
||||
if (hasCustomDebugKeystore()) {
|
||||
storeFile new File(getDebugKeystoreFile())
|
||||
storePassword getDebugKeystorePassword()
|
||||
keyAlias getDebugKeyAlias()
|
||||
keyPassword getDebugKeystorePassword()
|
||||
}
|
||||
}
|
||||
|
||||
release {
|
||||
File keystoreFile = new File(getReleaseKeystoreFile())
|
||||
if (keystoreFile.isFile()) {
|
||||
storeFile keystoreFile
|
||||
storePassword getReleaseKeystorePassword()
|
||||
keyAlias getReleaseKeyAlias()
|
||||
keyPassword getReleaseKeystorePassword()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
debug {
|
||||
// Signing and zip-aligning are skipped for prebuilt builds, but
|
||||
// performed for Godot gradle builds.
|
||||
zipAlignEnabled shouldZipAlign()
|
||||
if (shouldSign()) {
|
||||
signingConfig signingConfigs.debug
|
||||
} else {
|
||||
signingConfig null
|
||||
}
|
||||
}
|
||||
|
||||
release {
|
||||
// Signing and zip-aligning are skipped for prebuilt builds, but
|
||||
// performed for Godot gradle builds.
|
||||
zipAlignEnabled shouldZipAlign()
|
||||
if (shouldSign()) {
|
||||
signingConfig signingConfigs.release
|
||||
} else {
|
||||
signingConfig null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions 'edition'
|
||||
|
||||
productFlavors {
|
||||
// Product flavor for the standard (no .net support) builds.
|
||||
standard {
|
||||
getIsDefault().set(true)
|
||||
}
|
||||
|
||||
// Product flavor for the Mono (.net) builds.
|
||||
mono {}
|
||||
|
||||
// Product flavor used for running instrumented tests.
|
||||
instrumented {
|
||||
applicationIdSuffix ".instrumented"
|
||||
versionNameSuffix "-instrumented"
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.res.srcDirs += ['res']
|
||||
debug.jniLibs.srcDirs = ['libs/debug', 'libs/debug/vulkan_validation_layers']
|
||||
release.jniLibs.srcDirs = ['libs/release']
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.all { output ->
|
||||
String filenameSuffix = variant.flavorName == "mono" ? variant.name : variant.buildType.name
|
||||
output.outputFileName = "android_${filenameSuffix}.apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task copyAndRenameBinary(type: Copy) {
|
||||
// The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
|
||||
// and directories. Otherwise this check may cause permissions access failures on Windows
|
||||
// machines.
|
||||
doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
|
||||
|
||||
String exportPath = getExportPath()
|
||||
String exportFilename = getExportFilename()
|
||||
String exportEdition = getExportEdition()
|
||||
String exportBuildType = getExportBuildType()
|
||||
String exportBuildTypeCapitalized = exportBuildType.capitalize()
|
||||
String exportFormat = getExportFormat()
|
||||
|
||||
boolean isAab = exportFormat == "aab"
|
||||
boolean isMono = exportEdition == "mono"
|
||||
String filenameSuffix = isAab ? "${exportEdition}-${exportBuildType}" : exportBuildType
|
||||
if (isMono) {
|
||||
filenameSuffix = isAab ? "${exportEdition}-${exportBuildType}" : "${exportEdition}${exportBuildTypeCapitalized}"
|
||||
}
|
||||
|
||||
String sourceFilename = isAab ? "${project.name}-${filenameSuffix}.aab" : "android_${filenameSuffix}.apk"
|
||||
String sourceFilepath = isAab ? "$buildDir/outputs/bundle/${exportEdition}${exportBuildTypeCapitalized}/$sourceFilename" : "$buildDir/outputs/apk/$exportEdition/$exportBuildType/$sourceFilename"
|
||||
|
||||
from sourceFilepath
|
||||
into exportPath
|
||||
rename sourceFilename, exportFilename
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to validate the version of the Java SDK used for the Godot gradle builds.
|
||||
*/
|
||||
task validateJavaVersion {
|
||||
if (!JavaVersion.current().isCompatibleWith(versions.javaVersion)) {
|
||||
throw new GradleException("Invalid Java version ${JavaVersion.current()}. Version ${versions.javaVersion} is the minimum supported Java version for Godot gradle builds.")
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Older versions of our vendor plugin include a loader that we no longer need.
|
||||
* This code ensures those are removed.
|
||||
*/
|
||||
tasks.withType( com.android.build.gradle.internal.tasks.MergeNativeLibsTask) {
|
||||
doFirst {
|
||||
externalLibNativeLibs.each { jniDir ->
|
||||
if (jniDir.getCanonicalPath().contains("godot-openxr-") || jniDir.getCanonicalPath().contains("godotopenxr")) {
|
||||
// Delete the 'libopenxr_loader.so' files from the vendors plugin so we only use the version from the
|
||||
// openxr loader dependency.
|
||||
File armFile = new File(jniDir, "arm64-v8a/libopenxr_loader.so")
|
||||
if (armFile.exists()) {
|
||||
armFile.delete()
|
||||
}
|
||||
File x86File = new File(jniDir, "x86_64/libopenxr_loader.so")
|
||||
if (x86File.exists()) {
|
||||
x86File.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
When they're scheduled to run, the copy*AARToAppModule tasks generate dependencies for the 'app'
|
||||
module, so we're ensuring the ':app:preBuild' task is set to run after those tasks.
|
||||
*/
|
||||
if (rootProject.tasks.findByPath("copyDebugAARToAppModule") != null) {
|
||||
preBuild.mustRunAfter(rootProject.tasks.named("copyDebugAARToAppModule"))
|
||||
}
|
||||
if (rootProject.tasks.findByPath("copyReleaseAARToAppModule") != null) {
|
||||
preBuild.mustRunAfter(rootProject.tasks.named("copyReleaseAARToAppModule"))
|
||||
}
|
||||
411
engine/platform/android/java/app/config.gradle
Normal file
411
engine/platform/android/java/app/config.gradle
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
ext.versions = [
|
||||
androidGradlePlugin: '8.6.1',
|
||||
compileSdk : 36,
|
||||
// Also update:
|
||||
// - 'platform/android/export/export_plugin.cpp#DEFAULT_MIN_SDK_VERSION'
|
||||
// - 'platform/android/detect.py#get_min_target_api()'
|
||||
minSdk : 24,
|
||||
// Also update 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION'
|
||||
targetSdk : 36,
|
||||
buildTools : '36.1.0',
|
||||
kotlinVersion : '2.1.21',
|
||||
fragmentVersion : '1.8.6',
|
||||
nexusPublishVersion: '1.3.0',
|
||||
javaVersion : JavaVersion.VERSION_17,
|
||||
// Also update 'platform/android/detect.py#get_ndk_version()' when this is updated.
|
||||
ndkVersion : '29.0.14206865',
|
||||
splashscreenVersion: '1.0.1',
|
||||
// 'openxrLoaderVersion' should be set to XR_CURRENT_API_VERSION, see 'thirdparty/openxr'
|
||||
openxrLoaderVersion: '1.1.53',
|
||||
openxrVendorsVersion: '4.3.0-stable',
|
||||
junitVersion : '1.3.0',
|
||||
espressoCoreVersion: '3.7.0',
|
||||
kotlinTestVersion : '1.3.11',
|
||||
testRunnerVersion : '1.7.0',
|
||||
testOrchestratorVersion: '1.6.1',
|
||||
documentfileVersion: '1.1.0',
|
||||
]
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
ext.getExportVersionCode = { ->
|
||||
String versionCode = project.hasProperty("export_version_code") ? project.property("export_version_code") : ""
|
||||
if (versionCode == null || versionCode.isEmpty()) {
|
||||
versionCode = "1"
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(versionCode)
|
||||
} catch (NumberFormatException ignored) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ext.getExportVersionName = { ->
|
||||
String versionName = project.hasProperty("export_version_name") ? project.property("export_version_name") : ""
|
||||
if (versionName == null || versionName.isEmpty()) {
|
||||
versionName = "1.0"
|
||||
}
|
||||
return versionName
|
||||
}
|
||||
|
||||
ext.getExportMinSdkVersion = { ->
|
||||
String minSdkVersion = project.hasProperty("export_version_min_sdk") ? project.property("export_version_min_sdk") : ""
|
||||
if (minSdkVersion == null || minSdkVersion.isEmpty()) {
|
||||
minSdkVersion = "$versions.minSdk"
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(minSdkVersion)
|
||||
} catch (NumberFormatException ignored) {
|
||||
return versions.minSdk
|
||||
}
|
||||
}
|
||||
|
||||
ext.getExportTargetSdkVersion = { ->
|
||||
String targetSdkVersion = project.hasProperty("export_version_target_sdk") ? project.property("export_version_target_sdk") : ""
|
||||
if (targetSdkVersion == null || targetSdkVersion.isEmpty()) {
|
||||
targetSdkVersion = "$versions.targetSdk"
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(targetSdkVersion)
|
||||
} catch (NumberFormatException ignored) {
|
||||
return versions.targetSdk
|
||||
}
|
||||
}
|
||||
|
||||
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 ->
|
||||
// Attempt to read the version from the `version.py` file.
|
||||
String libraryVersionName = ""
|
||||
int libraryVersionCode = 0
|
||||
|
||||
File versionFile = new File("../../../version.py")
|
||||
if (versionFile.isFile()) {
|
||||
def map = [:]
|
||||
|
||||
List<String> lines = versionFile.readLines()
|
||||
for (String line in lines) {
|
||||
String[] keyValue = line.split("=")
|
||||
String key = keyValue[0].trim()
|
||||
String value = keyValue[1].trim().replaceAll("\"", "")
|
||||
|
||||
if (requiredKeys.contains(key)) {
|
||||
if (!value.isEmpty()) {
|
||||
map[key] = value
|
||||
}
|
||||
requiredKeys.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
if (requiredKeys.empty) {
|
||||
libraryVersionName = map.values().join(".")
|
||||
try {
|
||||
if (map.containsKey("status")) {
|
||||
int statusCode = 0
|
||||
String statusValue = map["status"]
|
||||
if (statusValue == null) {
|
||||
statusCode = 0
|
||||
} else if (statusValue.startsWith("dev")) {
|
||||
statusCode = 1
|
||||
} else if (statusValue.startsWith("alpha")) {
|
||||
statusCode = 2
|
||||
} else if (statusValue.startsWith("beta")) {
|
||||
statusCode = 3
|
||||
} else if (statusValue.startsWith("rc")) {
|
||||
statusCode = 4
|
||||
} else if (statusValue.startsWith("stable")) {
|
||||
statusCode = 5
|
||||
} else {
|
||||
statusCode = 0
|
||||
}
|
||||
|
||||
libraryVersionCode = statusCode
|
||||
}
|
||||
|
||||
if (map.containsKey("patch")) {
|
||||
libraryVersionCode += Integer.parseInt(map["patch"]) * 10
|
||||
}
|
||||
|
||||
if (map.containsKey("minor")) {
|
||||
libraryVersionCode += (Integer.parseInt(map["minor"]) * 1000)
|
||||
}
|
||||
|
||||
if (map.containsKey("major")) {
|
||||
libraryVersionCode += (Integer.parseInt(map["major"]) * 100000)
|
||||
}
|
||||
} catch (NumberFormatException ignore) {
|
||||
libraryVersionCode = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (libraryVersionName.isEmpty()) {
|
||||
// Fallback value in case we're unable to read the file.
|
||||
libraryVersionName = "custom_build"
|
||||
}
|
||||
|
||||
if (libraryVersionCode == 0) {
|
||||
libraryVersionCode = 1
|
||||
}
|
||||
|
||||
return [libraryVersionName, libraryVersionCode]
|
||||
}
|
||||
|
||||
ext.getGodotLibraryVersion = { ->
|
||||
List<String> requiredKeys = ["major", "minor", "patch", "status", "module_config"]
|
||||
return generateGodotLibraryVersion(requiredKeys)
|
||||
}
|
||||
|
||||
ext.getGodotPublishVersion = { ->
|
||||
List<String> requiredKeys = ["major", "minor", "patch", "status"]
|
||||
String versionName = ""
|
||||
int versionCode = 1
|
||||
(versionName, versionCode) = generateGodotLibraryVersion(requiredKeys)
|
||||
if (!versionName.endsWith("stable")) {
|
||||
versionName += "-SNAPSHOT"
|
||||
}
|
||||
return versionName
|
||||
}
|
||||
|
||||
final String VALUE_SEPARATOR_REGEX = "\\|"
|
||||
|
||||
// get the list of ABIs the project should be exported to
|
||||
ext.getExportEnabledABIs = { ->
|
||||
String enabledABIs = project.hasProperty("export_enabled_abis") ? project.property("export_enabled_abis") : ""
|
||||
if (enabledABIs == null || enabledABIs.isEmpty()) {
|
||||
enabledABIs = "armeabi-v7a|arm64-v8a|x86|x86_64|"
|
||||
}
|
||||
Set<String> exportAbiFilter = []
|
||||
for (String abi_name : enabledABIs.split(VALUE_SEPARATOR_REGEX)) {
|
||||
if (!abi_name.trim().isEmpty()){
|
||||
exportAbiFilter.add(abi_name)
|
||||
}
|
||||
}
|
||||
return exportAbiFilter
|
||||
}
|
||||
|
||||
ext.getExportPath = {
|
||||
String exportPath = project.hasProperty("export_path") ? project.property("export_path") : ""
|
||||
if (exportPath == null || exportPath.isEmpty()) {
|
||||
exportPath = "."
|
||||
}
|
||||
return exportPath
|
||||
}
|
||||
|
||||
ext.getExportFilename = {
|
||||
String exportFilename = project.hasProperty("export_filename") ? project.property("export_filename") : ""
|
||||
if (exportFilename == null || exportFilename.isEmpty()) {
|
||||
exportFilename = "godot_android"
|
||||
}
|
||||
return exportFilename
|
||||
}
|
||||
|
||||
ext.getExportEdition = {
|
||||
String exportEdition = project.hasProperty("export_edition") ? project.property("export_edition") : ""
|
||||
if (exportEdition == null || exportEdition.isEmpty()) {
|
||||
exportEdition = "standard"
|
||||
}
|
||||
return exportEdition
|
||||
}
|
||||
|
||||
ext.getExportBuildType = {
|
||||
String exportBuildType = project.hasProperty("export_build_type") ? project.property("export_build_type") : ""
|
||||
if (exportBuildType == null || exportBuildType.isEmpty()) {
|
||||
exportBuildType = "debug"
|
||||
}
|
||||
return exportBuildType
|
||||
}
|
||||
|
||||
ext.getExportFormat = {
|
||||
String exportFormat = project.hasProperty("export_format") ? project.property("export_format") : ""
|
||||
if (exportFormat == null || exportFormat.isEmpty()) {
|
||||
exportFormat = "apk"
|
||||
}
|
||||
return exportFormat
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the project properties for the 'plugins_maven_repos' property and return the list
|
||||
* of maven repos.
|
||||
*/
|
||||
ext.getGodotPluginsMavenRepos = { ->
|
||||
Set<String> mavenRepos = []
|
||||
|
||||
// Retrieve the list of maven repos.
|
||||
if (project.hasProperty("plugins_maven_repos")) {
|
||||
String mavenReposProperty = project.property("plugins_maven_repos")
|
||||
if (mavenReposProperty != null && !mavenReposProperty.trim().isEmpty()) {
|
||||
for (String mavenRepoUrl : mavenReposProperty.split(VALUE_SEPARATOR_REGEX)) {
|
||||
mavenRepos += mavenRepoUrl.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mavenRepos
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the project properties for the 'plugins_remote_binaries' property and return
|
||||
* it for inclusion in the build dependencies.
|
||||
*/
|
||||
ext.getGodotPluginsRemoteBinaries = { ->
|
||||
Set<String> remoteDeps = []
|
||||
|
||||
// Retrieve the list of remote plugins binaries.
|
||||
if (project.hasProperty("plugins_remote_binaries")) {
|
||||
String remoteDepsList = project.property("plugins_remote_binaries")
|
||||
if (remoteDepsList != null && !remoteDepsList.trim().isEmpty()) {
|
||||
for (String dep: remoteDepsList.split(VALUE_SEPARATOR_REGEX)) {
|
||||
remoteDeps += dep.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
return remoteDeps
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the project properties for the 'plugins_local_binaries' property and return
|
||||
* their binaries for inclusion in the build dependencies.
|
||||
*/
|
||||
ext.getGodotPluginsLocalBinaries = { ->
|
||||
Set<String> binDeps = []
|
||||
|
||||
// Retrieve the list of local plugins binaries.
|
||||
if (project.hasProperty("plugins_local_binaries")) {
|
||||
String pluginsList = project.property("plugins_local_binaries")
|
||||
if (pluginsList != null && !pluginsList.trim().isEmpty()) {
|
||||
for (String plugin : pluginsList.split(VALUE_SEPARATOR_REGEX)) {
|
||||
binDeps += plugin.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return binDeps
|
||||
}
|
||||
|
||||
ext.getDebugKeystoreFile = { ->
|
||||
String keystoreFile = project.hasProperty("debug_keystore_file") ? project.property("debug_keystore_file") : ""
|
||||
if (keystoreFile == null || keystoreFile.isEmpty()) {
|
||||
keystoreFile = "."
|
||||
}
|
||||
return keystoreFile
|
||||
}
|
||||
|
||||
ext.hasCustomDebugKeystore = { ->
|
||||
File keystoreFile = new File(getDebugKeystoreFile())
|
||||
return keystoreFile.isFile()
|
||||
}
|
||||
|
||||
ext.getDebugKeystorePassword = { ->
|
||||
String keystorePassword = project.hasProperty("debug_keystore_password") ? project.property("debug_keystore_password") : ""
|
||||
if (keystorePassword == null || keystorePassword.isEmpty()) {
|
||||
keystorePassword = "android"
|
||||
}
|
||||
return keystorePassword
|
||||
}
|
||||
|
||||
ext.getDebugKeyAlias = { ->
|
||||
String keyAlias = project.hasProperty("debug_keystore_alias") ? project.property("debug_keystore_alias") : ""
|
||||
if (keyAlias == null || keyAlias.isEmpty()) {
|
||||
keyAlias = "androiddebugkey"
|
||||
}
|
||||
return keyAlias
|
||||
}
|
||||
|
||||
ext.getReleaseKeystoreFile = { ->
|
||||
String keystoreFile = project.hasProperty("release_keystore_file") ? project.property("release_keystore_file") : ""
|
||||
if (keystoreFile == null || keystoreFile.isEmpty()) {
|
||||
keystoreFile = "."
|
||||
}
|
||||
return keystoreFile
|
||||
}
|
||||
|
||||
ext.getReleaseKeystorePassword = { ->
|
||||
String keystorePassword = project.hasProperty("release_keystore_password") ? project.property("release_keystore_password") : ""
|
||||
return keystorePassword
|
||||
}
|
||||
|
||||
ext.getReleaseKeyAlias = { ->
|
||||
String keyAlias = project.hasProperty("release_keystore_alias") ? project.property("release_keystore_alias") : ""
|
||||
return keyAlias
|
||||
}
|
||||
|
||||
ext.isAndroidStudio = { ->
|
||||
return project.hasProperty('android.injected.invoked.from.ide')
|
||||
}
|
||||
|
||||
ext.shouldZipAlign = { ->
|
||||
String zipAlignFlag = project.hasProperty("perform_zipalign") ? project.property("perform_zipalign") : ""
|
||||
if (zipAlignFlag == null || zipAlignFlag.isEmpty()) {
|
||||
if (isAndroidStudio()) {
|
||||
zipAlignFlag = "true"
|
||||
} else {
|
||||
zipAlignFlag = "false"
|
||||
}
|
||||
}
|
||||
return Boolean.parseBoolean(zipAlignFlag)
|
||||
}
|
||||
|
||||
ext.shouldSign = { ->
|
||||
String signFlag = project.hasProperty("perform_signing") ? project.property("perform_signing") : ""
|
||||
if (signFlag == null || signFlag.isEmpty()) {
|
||||
if (isAndroidStudio()) {
|
||||
signFlag = "true"
|
||||
} else {
|
||||
signFlag = "false"
|
||||
}
|
||||
}
|
||||
return Boolean.parseBoolean(signFlag)
|
||||
}
|
||||
|
||||
ext.shouldNotStrip = { ->
|
||||
return isAndroidStudio() || project.hasProperty("doNotStrip")
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to use the legacy convention of compressing all .so files in the APK.
|
||||
*
|
||||
* For more background, see:
|
||||
* - https://developer.android.com/build/releases/past-releases/agp-3-6-0-release-notes#extractNativeLibs
|
||||
* - https://stackoverflow.com/a/44704840
|
||||
*/
|
||||
ext.shouldUseLegacyPackaging = { ->
|
||||
String legacyPackagingFlag = project.hasProperty("compress_native_libraries") ? project.property("compress_native_libraries") : ""
|
||||
if (legacyPackagingFlag != null && !legacyPackagingFlag.isEmpty()) {
|
||||
return Boolean.parseBoolean(legacyPackagingFlag)
|
||||
}
|
||||
|
||||
if (getExportMinSdkVersion() <= 29) {
|
||||
// Use legacy packaging for compatibility with device running api <= 29.
|
||||
// See https://github.com/godotengine/godot/issues/108842 for reference.
|
||||
return true
|
||||
}
|
||||
|
||||
// Default behavior for minSdk > 29.
|
||||
return false
|
||||
}
|
||||
|
||||
ext.getAddonsDirectory = { ->
|
||||
String addonsDirectory = project.hasProperty("addons_directory") ? project.property("addons_directory") : ""
|
||||
return addonsDirectory
|
||||
}
|
||||
28
engine/platform/android/java/app/gradle.properties
Normal file
28
engine/platform/android/java/app/gradle.properties
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Godot gradle build settings.
|
||||
# These properties apply when running a gradle build from the Godot editor.
|
||||
# NOTE: This should be kept in sync with 'godot/platform/android/java/gradle.properties' except
|
||||
# where otherwise specified.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# https://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx4536m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# https://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
org.gradle.warning.mode=all
|
||||
|
||||
# Enable resource optimizations for release build.
|
||||
# NOTE: This is turned off for template release build in order to support the build legacy process.
|
||||
android.enableResourceOptimizations=true
|
||||
|
||||
# Fix gradle build errors when the build path contains non-ASCII characters
|
||||
android.overridePathCheck=true
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-ar</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-bg</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-ca</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-cs</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-da</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-de</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-el</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-en</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-es_ES</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-es</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-fa</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-fi</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-fr</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-hi</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-hr</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-hu</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-in</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-it</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-iw</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-ja</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-ko</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-lt</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-lv</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-nb</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-nl</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-pl</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-pt</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-ro</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-ru</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-sk</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-sl</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-sr</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-sv</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-th</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-tl</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-tr</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-uk</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-vi</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-zh_HK</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-zh_TW</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-zh</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name</string>
|
||||
</resources>
|
||||
18
engine/platform/android/java/app/res/values/themes.xml
Normal file
18
engine/platform/android/java/app/res/values/themes.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- GodotAppMainTheme is auto-generated during export. Manual changes will be overwritten.
|
||||
To add custom attributes, use the "gradle_build/custom_theme_attributes" Android export option. -->
|
||||
<style name="GodotAppMainTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar">
|
||||
<item name="android:windowSwipeToDismiss">false</item>
|
||||
<item name="android:windowIsTranslucent">false</item>
|
||||
</style>
|
||||
|
||||
<!-- GodotAppSplashTheme is auto-generated during export. Manual changes will be overwritten.
|
||||
To add custom attributes, use the "gradle_build/custom_theme_attributes" Android export option. -->
|
||||
<style name="GodotAppSplashTheme" parent="Theme.SplashScreen">
|
||||
<item name="android:windowSplashScreenBackground">@mipmap/icon_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/icon_foreground</item>
|
||||
<item name="postSplashScreenTheme">@style/GodotAppMainTheme</item>
|
||||
<item name="android:windowIsTranslucent">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
18
engine/platform/android/java/app/settings.gradle
Normal file
18
engine/platform/android/java/app/settings.gradle
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// This is the root directory of the Godot Android gradle build.
|
||||
pluginManagement {
|
||||
apply from: 'config.gradle'
|
||||
|
||||
plugins {
|
||||
id 'com.android.application' version versions.androidGradlePlugin
|
||||
id 'org.jetbrains.kotlin.android' version versions.kotlinVersion
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
maven { url "https://central.sonatype.com/repository/maven-snapshots/"}
|
||||
}
|
||||
}
|
||||
|
||||
include ':assetPackInstallTime'
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
/**************************************************************************/
|
||||
/* GodotAppTest.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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 com.godot.game
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.godot.game.test.GodotAppInstrumentedTestPlugin
|
||||
import org.godotengine.godot.GodotActivity.Companion.EXTRA_COMMAND_LINE_PARAMS
|
||||
import org.godotengine.godot.plugin.GodotPluginRegistry
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* This instrumented test will launch the `instrumented` version of GodotApp and run a set of tests against it.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class GodotAppTest {
|
||||
|
||||
companion object {
|
||||
private val TAG = GodotAppTest::class.java.simpleName
|
||||
|
||||
private const val GODOT_APP_LAUNCHER_CLASS_NAME = "com.godot.game.GodotAppLauncher"
|
||||
private const val GODOT_APP_CLASS_NAME = "com.godot.game.GodotApp"
|
||||
|
||||
private val TEST_COMMAND_LINE_PARAMS = arrayOf("This is a test")
|
||||
}
|
||||
|
||||
private fun getTestPlugin(): GodotAppInstrumentedTestPlugin? {
|
||||
return GodotPluginRegistry.getPluginRegistry()
|
||||
.getPlugin("GodotAppInstrumentedTestPlugin") as GodotAppInstrumentedTestPlugin?
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the JavaClassWrapper tests via the GodotAppInstrumentedTestPlugin.
|
||||
*/
|
||||
@Test
|
||||
fun runJavaClassWrapperTests() {
|
||||
ActivityScenario.launch(GodotApp::class.java).use { scenario ->
|
||||
scenario.onActivity { activity ->
|
||||
val testPlugin = getTestPlugin()
|
||||
assertNotNull(testPlugin)
|
||||
|
||||
Log.d(TAG, "Waiting for the Godot main loop to start...")
|
||||
testPlugin.waitForGodotMainLoopStarted()
|
||||
|
||||
Log.d(TAG, "Running JavaClassWrapper tests...")
|
||||
val result = testPlugin.runJavaClassWrapperTests()
|
||||
assertNotNull(result)
|
||||
result.exceptionOrNull()?.let { throw it }
|
||||
assertTrue(result.isSuccess)
|
||||
Log.d(TAG, "Passed ${result.getOrNull()} tests")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs file access related tests.
|
||||
*/
|
||||
@Test
|
||||
fun runFileAccessTests() {
|
||||
ActivityScenario.launch(GodotApp::class.java).use { scenario ->
|
||||
scenario.onActivity { activity ->
|
||||
val testPlugin = getTestPlugin()
|
||||
assertNotNull(testPlugin)
|
||||
|
||||
Log.d(TAG, "Waiting for the Godot main loop to start...")
|
||||
testPlugin.waitForGodotMainLoopStarted()
|
||||
|
||||
Log.d(TAG, "Running FileAccess tests...")
|
||||
val result = testPlugin.runFileAccessTests()
|
||||
assertNotNull(result)
|
||||
result.exceptionOrNull()?.let { throw it }
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test implicit launch of the Godot app, and validates this resolves to the `GodotAppLauncher` activity alias.
|
||||
*/
|
||||
@Test
|
||||
fun testImplicitGodotAppLauncherLaunch() {
|
||||
val implicitLaunchIntent = Intent().apply {
|
||||
setPackage(BuildConfig.APPLICATION_ID)
|
||||
action = Intent.ACTION_MAIN
|
||||
addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
putExtra(EXTRA_COMMAND_LINE_PARAMS, TEST_COMMAND_LINE_PARAMS)
|
||||
}
|
||||
ActivityScenario.launch<GodotApp>(implicitLaunchIntent).use { scenario ->
|
||||
scenario.onActivity { activity ->
|
||||
assertEquals(activity.intent.component?.className, GODOT_APP_LAUNCHER_CLASS_NAME)
|
||||
|
||||
val commandLineParams = activity.intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
|
||||
assertNull(commandLineParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test explicit launch of the Godot app via its activity-alias launcher, and validates it resolves properly.
|
||||
*/
|
||||
@Test
|
||||
fun testExplicitGodotAppLauncherLaunch() {
|
||||
val explicitIntent = Intent().apply {
|
||||
component = ComponentName(BuildConfig.APPLICATION_ID, GODOT_APP_LAUNCHER_CLASS_NAME)
|
||||
putExtra(EXTRA_COMMAND_LINE_PARAMS, TEST_COMMAND_LINE_PARAMS)
|
||||
}
|
||||
ActivityScenario.launch<GodotApp>(explicitIntent).use { scenario ->
|
||||
scenario.onActivity { activity ->
|
||||
assertEquals(activity.intent.component?.className, GODOT_APP_LAUNCHER_CLASS_NAME)
|
||||
|
||||
val commandLineParams = activity.intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
|
||||
assertNull(commandLineParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test explicit launch of the `GodotApp` activity.
|
||||
*/
|
||||
@Test
|
||||
fun testExplicitGodotAppLaunch() {
|
||||
val explicitIntent = Intent().apply {
|
||||
component = ComponentName(BuildConfig.APPLICATION_ID, GODOT_APP_CLASS_NAME)
|
||||
putExtra(EXTRA_COMMAND_LINE_PARAMS, TEST_COMMAND_LINE_PARAMS)
|
||||
}
|
||||
ActivityScenario.launch<GodotApp>(explicitIntent).use { scenario ->
|
||||
scenario.onActivity { activity ->
|
||||
assertEquals(activity.intent.component?.className, GODOT_APP_CLASS_NAME)
|
||||
|
||||
val commandLineParams = activity.intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
|
||||
assertNotNull(commandLineParams)
|
||||
assertTrue(commandLineParams.contentEquals(TEST_COMMAND_LINE_PARAMS))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<meta-data
|
||||
android:name="org.godotengine.plugin.v2.GodotAppInstrumentedTestPlugin"
|
||||
android:value="com.godot.game.test.GodotAppInstrumentedTestPlugin"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
2
engine/platform/android/java/app/src/instrumented/assets/.gitattributes
vendored
Normal file
2
engine/platform/android/java/app/src/instrumented/assets/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Normalize EOL for all files that Git considers text files.
|
||||
* text=auto eol=lf
|
||||
3
engine/platform/android/java/app/src/instrumented/assets/.gitignore
vendored
Normal file
3
engine/platform/android/java/app/src/instrumented/assets/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Godot 4+ specific ignores
|
||||
/android/
|
||||
/.godot/editor
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
list=[{
|
||||
"base": &"RefCounted",
|
||||
"class": &"BaseTest",
|
||||
"icon": "",
|
||||
"is_abstract": true,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://test/base_test.gd"
|
||||
}, {
|
||||
"base": &"BaseTest",
|
||||
"class": &"FileAccessTests",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://test/file_access/file_access_tests.gd"
|
||||
}, {
|
||||
"base": &"BaseTest",
|
||||
"class": &"JavaClassWrapperTests",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://test/javaclasswrapper/java_class_wrapper_tests.gd"
|
||||
}]
|
||||
Binary file not shown.
|
|
@ -0,0 +1,2 @@
|
|||
source_md5="4cdc64b13a9af63279c486903c9b54cc"
|
||||
dest_md5="ddbdfc47e6405ad8d8e9e6a88a32824e"
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 813 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H447l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c3 34 55 34 58 0v-86c-3-34-55-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
|
||||
|
After Width: | Height: | Size: 996 B |
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://srnrli5m8won"
|
||||
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
extends Node2D
|
||||
|
||||
var _plugin_name = "GodotAppInstrumentedTestPlugin"
|
||||
var _android_plugin
|
||||
|
||||
func _ready():
|
||||
if Engine.has_singleton(_plugin_name):
|
||||
_android_plugin = Engine.get_singleton(_plugin_name)
|
||||
_android_plugin.connect("launch_tests", _launch_tests)
|
||||
else:
|
||||
printerr("Couldn't find plugin " + _plugin_name)
|
||||
get_tree().quit()
|
||||
|
||||
func _launch_tests(test_label: String) -> void:
|
||||
var test_instance: BaseTest = null
|
||||
match test_label:
|
||||
"javaclasswrapper_tests":
|
||||
test_instance = JavaClassWrapperTests.new()
|
||||
"file_access_tests":
|
||||
test_instance = FileAccessTests.new()
|
||||
|
||||
if test_instance:
|
||||
test_instance.__reset_tests()
|
||||
test_instance.run_tests()
|
||||
var incomplete_tests = test_instance._test_started - test_instance._test_completed
|
||||
_android_plugin.onTestsCompleted(test_label, test_instance._test_completed, test_instance._test_assert_failures + incomplete_tests)
|
||||
else:
|
||||
_android_plugin.onTestsFailed(test_label, "Unable to launch tests")
|
||||
|
||||
|
||||
func _on_plugin_toast_button_pressed() -> void:
|
||||
if _android_plugin:
|
||||
_android_plugin.helloWorld()
|
||||
|
||||
func _on_vibration_button_pressed() -> void:
|
||||
var android_runtime = Engine.get_singleton("AndroidRuntime")
|
||||
if android_runtime:
|
||||
print("Checking if the device supports vibration")
|
||||
var vibrator_service = android_runtime.getApplicationContext().getSystemService("vibrator")
|
||||
if vibrator_service:
|
||||
if vibrator_service.hasVibrator():
|
||||
print("Vibration is supported on device! Vibrating now...")
|
||||
var VibrationEffect = JavaClassWrapper.wrap("android.os.VibrationEffect")
|
||||
var effect = VibrationEffect.createOneShot(500, VibrationEffect.DEFAULT_AMPLITUDE)
|
||||
vibrator_service.vibrate(effect)
|
||||
else:
|
||||
printerr("Vibration is not supported on device")
|
||||
else:
|
||||
printerr("Unable to retrieve the vibrator service")
|
||||
else:
|
||||
printerr("Couldn't find AndroidRuntime singleton")
|
||||
|
||||
func _on_gd_script_toast_button_pressed() -> void:
|
||||
var android_runtime = Engine.get_singleton("AndroidRuntime")
|
||||
if android_runtime:
|
||||
var activity = android_runtime.getActivity()
|
||||
|
||||
var toastCallable = func ():
|
||||
var ToastClass = JavaClassWrapper.wrap("android.widget.Toast")
|
||||
ToastClass.makeText(activity, "Toast from GDScript", ToastClass.LENGTH_LONG).show()
|
||||
|
||||
activity.runOnUiThread(android_runtime.createRunnableFromGodotCallable(toastCallable))
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://bv6y7in6otgcm
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://cg3hylang5fxn"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bv6y7in6otgcm" path="res://main.gd" id="1_j0gfq"]
|
||||
|
||||
[node name="Main" type="Node2D"]
|
||||
script = ExtResource("1_j0gfq")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
offset_left = 68.0
|
||||
offset_top = 102.0
|
||||
offset_right = 506.0
|
||||
offset_bottom = 408.0
|
||||
theme_override_constants/separation = 25
|
||||
|
||||
[node name="PluginToastButton" type="Button" parent="VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 50)
|
||||
layout_mode = 2
|
||||
text = "Plugin Toast
|
||||
"
|
||||
|
||||
[node name="VibrationButton" type="Button" parent="VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 50)
|
||||
layout_mode = 2
|
||||
text = "Vibration"
|
||||
|
||||
[node name="GDScriptToastButton" type="Button" parent="VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 50)
|
||||
layout_mode = 2
|
||||
text = "GDScript Toast
|
||||
"
|
||||
|
||||
[connection signal="pressed" from="VBoxContainer/PluginToastButton" to="." method="_on_plugin_toast_button_pressed"]
|
||||
[connection signal="pressed" from="VBoxContainer/VibrationButton" to="." method="_on_vibration_button_pressed"]
|
||||
[connection signal="pressed" from="VBoxContainer/GDScriptToastButton" to="." method="_on_gd_script_toast_button_pressed"]
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="Godot App Instrumentation Tests"
|
||||
run/main_scene="res://main.tscn"
|
||||
config/features=PackedStringArray("4.5", "GL Compatibility")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[debug]
|
||||
|
||||
settings/stdout/verbose_stdout=true
|
||||
|
||||
[rendering]
|
||||
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
renderer/rendering_method.mobile="gl_compatibility"
|
||||
textures/vram_compression/import_etc2_astc=true
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
@abstract class_name BaseTest
|
||||
|
||||
var _test_started := 0
|
||||
var _test_completed := 0
|
||||
var _test_assert_passes := 0
|
||||
var _test_assert_failures := 0
|
||||
|
||||
@abstract func run_tests()
|
||||
|
||||
func __exec_test(test_func: Callable):
|
||||
_test_started += 1
|
||||
var ret = test_func.call()
|
||||
if ret == true:
|
||||
_test_completed += 1
|
||||
|
||||
func __reset_tests():
|
||||
_test_started = 0
|
||||
_test_completed = 0
|
||||
_test_assert_passes = 0
|
||||
_test_assert_failures = 0
|
||||
|
||||
func __get_stack_frame():
|
||||
for s in get_stack():
|
||||
if not s.function.begins_with('__') and s.function != "assert_equal":
|
||||
return s
|
||||
return null
|
||||
|
||||
func __assert_pass():
|
||||
_test_assert_passes += 1
|
||||
pass
|
||||
|
||||
func __assert_fail():
|
||||
_test_assert_failures += 1
|
||||
var s = __get_stack_frame()
|
||||
if s != null:
|
||||
print_rich ("[color=red] == FAILURE: In function %s() from '%s' on line %s[/color]" % [s.function, s.source, s.line])
|
||||
else:
|
||||
print_rich ("[color=red] == FAILURE (run with --debug to get more information!) ==[/color]")
|
||||
|
||||
func assert_equal(actual, expected):
|
||||
if actual == expected:
|
||||
__assert_pass()
|
||||
else:
|
||||
__assert_fail()
|
||||
print (" |-> Expected '%s' but got '%s'" % [expected, actual])
|
||||
|
||||
func assert_true(value):
|
||||
if value:
|
||||
__assert_pass()
|
||||
else:
|
||||
__assert_fail()
|
||||
print (" |-> Expected '%s' to be truthy" % value)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://mofa8j0d801f
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
class_name FileAccessTests
|
||||
extends BaseTest
|
||||
|
||||
const FILE_CONTENT = "This is a test for reading / writing to the "
|
||||
|
||||
func run_tests():
|
||||
print("FileAccess tests starting...")
|
||||
__exec_test(test_obb_dir_access)
|
||||
__exec_test(test_internal_app_dir_access)
|
||||
__exec_test(test_internal_cache_dir_access)
|
||||
__exec_test(test_external_app_dir_access)
|
||||
|
||||
# Scoped storage: Testing access to Downloads and Documents directory.
|
||||
var version = JavaClassWrapper.wrap("android.os.Build$VERSION")
|
||||
if version.SDK_INT >= 29:
|
||||
__exec_test(test_downloads_dir_access)
|
||||
__exec_test(test_documents_dir_access)
|
||||
|
||||
func _test_dir_access(dir_path: String, data_file_content: String) -> bool:
|
||||
print("Testing access to " + dir_path)
|
||||
var data_file_path = dir_path.path_join("data.dat")
|
||||
|
||||
var data_file = FileAccess.open(data_file_path, FileAccess.WRITE)
|
||||
assert_true(data_file != null)
|
||||
assert_true(data_file.store_string(data_file_content))
|
||||
data_file.close()
|
||||
|
||||
data_file = FileAccess.open(data_file_path, FileAccess.READ)
|
||||
assert_true(data_file != null)
|
||||
var file_content = data_file.get_as_text()
|
||||
assert_equal(file_content, data_file_content)
|
||||
data_file.close()
|
||||
|
||||
var deletion_result = DirAccess.remove_absolute(data_file_path)
|
||||
assert_equal(deletion_result, OK)
|
||||
return true
|
||||
|
||||
func test_obb_dir_access() -> bool:
|
||||
var android_runtime = Engine.get_singleton("AndroidRuntime")
|
||||
assert_true(android_runtime != null)
|
||||
|
||||
var app_context = android_runtime.getApplicationContext()
|
||||
var obb_dir: String = app_context.getObbDir().getCanonicalPath()
|
||||
_test_dir_access(obb_dir, FILE_CONTENT + "obb dir.")
|
||||
return true
|
||||
|
||||
func test_internal_app_dir_access() -> bool:
|
||||
var android_runtime = Engine.get_singleton("AndroidRuntime")
|
||||
assert_true(android_runtime != null)
|
||||
|
||||
var app_context = android_runtime.getApplicationContext()
|
||||
var internal_app_dir: String = app_context.getFilesDir().getCanonicalPath()
|
||||
_test_dir_access(internal_app_dir, FILE_CONTENT + "internal app dir.")
|
||||
return true
|
||||
|
||||
func test_internal_cache_dir_access() -> bool:
|
||||
var android_runtime = Engine.get_singleton("AndroidRuntime")
|
||||
assert_true(android_runtime != null)
|
||||
|
||||
var app_context = android_runtime.getApplicationContext()
|
||||
var internal_cache_dir: String = app_context.getCacheDir().getCanonicalPath()
|
||||
_test_dir_access(internal_cache_dir, FILE_CONTENT + "internal cache dir.")
|
||||
return true
|
||||
|
||||
func test_external_app_dir_access() -> bool:
|
||||
var android_runtime = Engine.get_singleton("AndroidRuntime")
|
||||
assert_true(android_runtime != null)
|
||||
|
||||
var app_context = android_runtime.getApplicationContext()
|
||||
var external_app_dir: String = app_context.getExternalFilesDir("").getCanonicalPath()
|
||||
_test_dir_access(external_app_dir, FILE_CONTENT + "external app dir.")
|
||||
return true
|
||||
|
||||
func test_downloads_dir_access() -> bool:
|
||||
var EnvironmentClass = JavaClassWrapper.wrap("android.os.Environment")
|
||||
var downloads_dir = EnvironmentClass.getExternalStoragePublicDirectory(EnvironmentClass.DIRECTORY_DOWNLOADS).getCanonicalPath()
|
||||
_test_dir_access(downloads_dir, FILE_CONTENT + "downloads dir.")
|
||||
return true
|
||||
|
||||
func test_documents_dir_access() -> bool:
|
||||
var EnvironmentClass = JavaClassWrapper.wrap("android.os.Environment")
|
||||
var documents_dir = EnvironmentClass.getExternalStoragePublicDirectory(EnvironmentClass.DIRECTORY_DOCUMENTS).getCanonicalPath()
|
||||
_test_dir_access(documents_dir, FILE_CONTENT + "documents dir.")
|
||||
return true
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://b1o8wj1s4vghn
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
class_name JavaClassWrapperTests
|
||||
extends BaseTest
|
||||
|
||||
func run_tests():
|
||||
print("JavaClassWrapper tests starting..")
|
||||
|
||||
__exec_test(test_exceptions)
|
||||
|
||||
__exec_test(test_multiple_signatures)
|
||||
__exec_test(test_array_arguments)
|
||||
__exec_test(test_array_return)
|
||||
|
||||
__exec_test(test_dictionary)
|
||||
|
||||
__exec_test(test_object_overload)
|
||||
|
||||
__exec_test(test_variant_conversion_safe_from_stack_overflow)
|
||||
|
||||
__exec_test(test_big_integers)
|
||||
|
||||
__exec_test(test_callable)
|
||||
|
||||
print("JavaClassWrapper tests finished.")
|
||||
print("Tests started: " + str(_test_started))
|
||||
print("Tests completed: " + str(_test_completed))
|
||||
|
||||
|
||||
func test_exceptions() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
#print(TestClass.get_java_method_list())
|
||||
|
||||
assert_equal(JavaClassWrapper.get_exception(), null)
|
||||
|
||||
assert_equal(TestClass.testExc(27), 0)
|
||||
assert_equal(str(JavaClassWrapper.get_exception()), '<JavaObject:java.lang.NullPointerException "java.lang.NullPointerException">')
|
||||
|
||||
assert_equal(JavaClassWrapper.get_exception(), null)
|
||||
|
||||
return true
|
||||
|
||||
func test_multiple_signatures() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
|
||||
var ai := [1, 2]
|
||||
assert_equal(TestClass.testMethod(1, ai), "IntArray: [1, 2]")
|
||||
|
||||
var astr := ["abc"]
|
||||
assert_equal(TestClass.testMethod(2, astr), "IntArray: [0]")
|
||||
|
||||
var atstr: Array[String] = ["abc"]
|
||||
assert_equal(TestClass.testMethod(3, atstr), "StringArray: [abc]")
|
||||
|
||||
var TestClass2: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass2')
|
||||
var aobjl: Array[Object] = [
|
||||
TestClass2.TestClass2(27),
|
||||
TestClass2.TestClass2(135),
|
||||
]
|
||||
assert_equal(TestClass.testMethod(3, aobjl), "testObjects: 27 135")
|
||||
|
||||
return true
|
||||
|
||||
func test_array_arguments() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
|
||||
assert_equal(TestClass.testArgBoolArray([true, false, true]), "[true, false, true]")
|
||||
assert_equal(TestClass.testArgByteArray(PackedByteArray([1, 2, 3])), "[1, 2, 3]")
|
||||
assert_equal(TestClass.testArgCharArray("abc".to_utf16_buffer()), "abc");
|
||||
assert_equal(TestClass.testArgShortArray(PackedInt32Array([27, 28, 29])), "[27, 28, 29]")
|
||||
assert_equal(TestClass.testArgShortArray([27, 28, 29]), "[27, 28, 29]")
|
||||
assert_equal(TestClass.testArgIntArray(PackedInt32Array([7, 8, 9])), "[7, 8, 9]")
|
||||
assert_equal(TestClass.testArgIntArray([7, 8, 9]), "[7, 8, 9]")
|
||||
assert_equal(TestClass.testArgLongArray(PackedInt64Array([17, 18, 19])), "[17, 18, 19]")
|
||||
assert_equal(TestClass.testArgLongArray([17, 18, 19]), "[17, 18, 19]")
|
||||
assert_equal(TestClass.testArgFloatArray(PackedFloat32Array([17.1, 18.2, 19.3])), "[17.1, 18.2, 19.3]")
|
||||
assert_equal(TestClass.testArgFloatArray([17.1, 18.2, 19.3]), "[17.1, 18.2, 19.3]")
|
||||
assert_equal(TestClass.testArgDoubleArray(PackedFloat64Array([37.1, 38.2, 39.3])), "[37.1, 38.2, 39.3]")
|
||||
assert_equal(TestClass.testArgDoubleArray([37.1, 38.2, 39.3]), "[37.1, 38.2, 39.3]")
|
||||
|
||||
return true
|
||||
|
||||
func test_array_return() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
#print(TestClass.get_java_method_list())
|
||||
|
||||
assert_equal(TestClass.testRetBoolArray(), [true, false, true])
|
||||
assert_equal(TestClass.testRetWrappedBoolArray(), [true, false, true])
|
||||
|
||||
assert_equal(TestClass.testRetByteArray(), PackedByteArray([1, 2, 3]))
|
||||
assert_equal(TestClass.testRetWrappedByteArray(), PackedByteArray([1, 2, 3]))
|
||||
|
||||
assert_equal(TestClass.testRetCharArray().get_string_from_utf16(), "abc")
|
||||
assert_equal(TestClass.testRetWrappedCharArray().get_string_from_utf16(), "abc")
|
||||
|
||||
assert_equal(TestClass.testRetShortArray(), PackedInt32Array([11, 12, 13]))
|
||||
assert_equal(TestClass.testRetWrappedShortArray(), PackedInt32Array([11, 12, 13]))
|
||||
|
||||
assert_equal(TestClass.testRetIntArray(), PackedInt32Array([21, 22, 23]))
|
||||
assert_equal(TestClass.testRetWrappedIntArray(), PackedInt32Array([21, 22, 23]))
|
||||
|
||||
assert_equal(TestClass.testRetLongArray(), PackedInt64Array([41, 42, 43]))
|
||||
assert_equal(TestClass.testRetWrappedLongArray(), PackedInt64Array([41, 42, 43]))
|
||||
|
||||
assert_equal(TestClass.testRetFloatArray(), PackedFloat32Array([31.1, 32.2, 33.3]))
|
||||
assert_equal(TestClass.testRetWrappedFloatArray(), PackedFloat32Array([31.1, 32.2, 33.3]))
|
||||
|
||||
assert_equal(TestClass.testRetDoubleArray(), PackedFloat64Array([41.1, 42.2, 43.3]))
|
||||
assert_equal(TestClass.testRetWrappedDoubleArray(), PackedFloat64Array([41.1, 42.2, 43.3]))
|
||||
|
||||
var obj_array = TestClass.testRetObjectArray()
|
||||
assert_equal(str(obj_array[0]), '<JavaObject:com.godot.game.test.javaclasswrapper.TestClass2 "51">')
|
||||
assert_equal(str(obj_array[1]), '<JavaObject:com.godot.game.test.javaclasswrapper.TestClass2 "52">')
|
||||
|
||||
assert_equal(TestClass.testRetStringArray(), PackedStringArray(["I", "am", "String"]))
|
||||
assert_equal(TestClass.testRetCharSequenceArray(), PackedStringArray(["I", "am", "CharSequence"]))
|
||||
|
||||
return true
|
||||
|
||||
func test_dictionary() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
assert_equal(TestClass.testDictionary({a = 1, b = 2}), "{a=1, b=2}")
|
||||
assert_equal(TestClass.testRetDictionary(), {a = 1, b = 2})
|
||||
assert_equal(TestClass.testRetDictionaryArray(), [{a = 1, b = 2}])
|
||||
assert_equal(TestClass.testDictionaryNested({a = 1, b = [2, 3], c = 4}), "{a: 1, b: [2, 3], c: 4}")
|
||||
return true
|
||||
|
||||
func test_object_overload() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
var TestClass2: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass2')
|
||||
var TestClass3: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass3')
|
||||
|
||||
var t2 = TestClass2.TestClass2(33)
|
||||
var t3 = TestClass3.TestClass3("thirty three")
|
||||
|
||||
assert_equal(TestClass.testObjectOverload(t2), "TestClass2: 33")
|
||||
assert_equal(TestClass.testObjectOverload(t3), "TestClass3: thirty three")
|
||||
|
||||
var arr_of_t2 = [t2, TestClass2.TestClass2(34)]
|
||||
var arr_of_t3 = [t3, TestClass3.TestClass3("thirty four")]
|
||||
|
||||
assert_equal(TestClass.testObjectOverloadArray(arr_of_t2), "TestClass2: [33, 34]")
|
||||
assert_equal(TestClass.testObjectOverloadArray(arr_of_t3), "TestClass3: [thirty three, thirty four]")
|
||||
return true
|
||||
|
||||
func test_variant_conversion_safe_from_stack_overflow() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
var arr: Array = [42]
|
||||
var dict: Dictionary = {"arr": arr}
|
||||
arr.append(dict)
|
||||
# The following line will crash with stack overflow if not handled property:
|
||||
TestClass.testDictionary(dict)
|
||||
return true
|
||||
|
||||
func test_big_integers() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
assert_equal(TestClass.testArgLong(4242424242), "4242424242")
|
||||
assert_equal(TestClass.testArgLong(-4242424242), "-4242424242")
|
||||
assert_equal(TestClass.testDictionary({a = 4242424242, b = -4242424242}), "{a=4242424242, b=-4242424242}")
|
||||
return true
|
||||
|
||||
func test_callable() -> bool:
|
||||
var android_runtime = Engine.get_singleton("AndroidRuntime")
|
||||
assert_true(android_runtime != null)
|
||||
|
||||
var cb1_data := {called = false}
|
||||
var cb1 = func():
|
||||
cb1_data['called'] = true
|
||||
return null
|
||||
android_runtime.createRunnableFromGodotCallable(cb1).run()
|
||||
assert_equal(cb1_data['called'], true)
|
||||
|
||||
return true
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://3ql82ggk41xc
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
/**************************************************************************/
|
||||
/* GodotAppInstrumentedTestPlugin.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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 com.godot.game.test
|
||||
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import org.godotengine.godot.Godot
|
||||
import org.godotengine.godot.plugin.GodotPlugin
|
||||
import org.godotengine.godot.plugin.UsedByGodot
|
||||
import org.godotengine.godot.plugin.SignalInfo
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
/**
|
||||
* [GodotPlugin] used to drive instrumented tests.
|
||||
*/
|
||||
class GodotAppInstrumentedTestPlugin(godot: Godot) : GodotPlugin(godot) {
|
||||
|
||||
companion object {
|
||||
private val TAG = GodotAppInstrumentedTestPlugin::class.java.simpleName
|
||||
private const val MAIN_LOOP_STARTED_LATCH_KEY = "main_loop_started_latch"
|
||||
|
||||
private const val JAVACLASSWRAPPER_TESTS = "javaclasswrapper_tests"
|
||||
private const val FILE_ACCESS_TESTS = "file_access_tests"
|
||||
|
||||
private val LAUNCH_TESTS_SIGNAL = SignalInfo("launch_tests", String::class.java)
|
||||
|
||||
private val SIGNALS = setOf(
|
||||
LAUNCH_TESTS_SIGNAL
|
||||
)
|
||||
}
|
||||
|
||||
private val testResults = ConcurrentHashMap<String, Result<Any>>()
|
||||
private val latches = ConcurrentHashMap<String, CountDownLatch>()
|
||||
|
||||
init {
|
||||
// Add a countdown latch that is triggered when `onGodotMainLoopStarted` is fired.
|
||||
// This will be used by tests to wait until the engine is ready.
|
||||
latches[MAIN_LOOP_STARTED_LATCH_KEY] = CountDownLatch(1)
|
||||
}
|
||||
|
||||
override fun getPluginName() = "GodotAppInstrumentedTestPlugin"
|
||||
|
||||
override fun getPluginSignals() = SIGNALS
|
||||
|
||||
override fun onGodotMainLoopStarted() {
|
||||
super.onGodotMainLoopStarted()
|
||||
latches.remove(MAIN_LOOP_STARTED_LATCH_KEY)?.countDown()
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the instrumented test to wait until the Godot main loop is up and running.
|
||||
*/
|
||||
internal fun waitForGodotMainLoopStarted() {
|
||||
// Wait on the CountDownLatch for `onGodotMainLoopStarted`
|
||||
try {
|
||||
latches[MAIN_LOOP_STARTED_LATCH_KEY]?.await()
|
||||
} catch (e: InterruptedException) {
|
||||
Log.e(TAG, "Unable to wait for Godot main loop started event.", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This launches the JavaClassWrapper tests, and wait until the tests are complete before returning.
|
||||
*/
|
||||
internal fun runJavaClassWrapperTests(): Result<Any>? {
|
||||
return launchTests(JAVACLASSWRAPPER_TESTS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the FileAccess tests, and wait until the tests are complete before returning.
|
||||
*/
|
||||
internal fun runFileAccessTests(): Result<Any>? {
|
||||
return launchTests(FILE_ACCESS_TESTS)
|
||||
}
|
||||
|
||||
private fun launchTests(testLabel: String): Result<Any>? {
|
||||
val latch = latches.getOrPut(testLabel) { CountDownLatch(1) }
|
||||
emitSignal(LAUNCH_TESTS_SIGNAL.name, testLabel)
|
||||
return try {
|
||||
latch.await()
|
||||
val result = testResults.remove(testLabel)
|
||||
result
|
||||
} catch (e: InterruptedException) {
|
||||
Log.e(TAG, "Unable to wait for completion for $testLabel", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked from gdscript when the tests are completed.
|
||||
*/
|
||||
@UsedByGodot
|
||||
fun onTestsCompleted(testLabel: String, passes: Int, failures: Int) {
|
||||
Log.d(TAG, "$testLabel tests completed")
|
||||
val result = if (failures == 0) {
|
||||
Result.success(passes)
|
||||
} else {
|
||||
Result.failure(AssertionError("$failures tests failed!"))
|
||||
}
|
||||
|
||||
completeTest(testLabel, result)
|
||||
}
|
||||
|
||||
@UsedByGodot
|
||||
fun onTestsFailed(testLabel: String, failureMessage: String) {
|
||||
Log.d(TAG, "$testLabel tests failed")
|
||||
val result: Result<Any> = Result.failure(AssertionError(failureMessage))
|
||||
completeTest(testLabel, result)
|
||||
}
|
||||
|
||||
private fun completeTest(testKey: String, result: Result<Any>) {
|
||||
testResults[testKey] = result
|
||||
latches.remove(testKey)?.countDown()
|
||||
}
|
||||
|
||||
@UsedByGodot
|
||||
fun helloWorld() {
|
||||
runOnHostThread {
|
||||
Toast.makeText(activity, "Toast from Android plugin", Toast.LENGTH_LONG).show()
|
||||
Log.v(pluginName, "Hello World")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
/**************************************************************************/
|
||||
/* TestClass.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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 com.godot.game.test.javaclasswrapper
|
||||
|
||||
import org.godotengine.godot.Dictionary
|
||||
import kotlin.collections.contentToString
|
||||
import kotlin.collections.joinToString
|
||||
|
||||
class TestClass {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun stringify(value: Any?): String {
|
||||
return when (value) {
|
||||
null -> "null"
|
||||
is Map<*, *> -> {
|
||||
val entries = value.entries.joinToString(", ") { (k, v) -> "${stringify(k)}: ${stringify(v)}" }
|
||||
"{$entries}"
|
||||
}
|
||||
|
||||
is List<*> -> value.joinToString(prefix = "[", postfix = "]") { stringify(it) }
|
||||
is Array<*> -> value.joinToString(prefix = "[", postfix = "]") { stringify(it) }
|
||||
is IntArray -> value.joinToString(prefix = "[", postfix = "]")
|
||||
is LongArray -> value.joinToString(prefix = "[", postfix = "]")
|
||||
is FloatArray -> value.joinToString(prefix = "[", postfix = "]")
|
||||
is DoubleArray -> value.joinToString(prefix = "[", postfix = "]")
|
||||
is BooleanArray -> value.joinToString(prefix = "[", postfix = "]")
|
||||
is CharArray -> value.joinToString(prefix = "[", postfix = "]")
|
||||
else -> value.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testDictionary(d: Dictionary): String {
|
||||
return d.toString()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testDictionaryNested(d: Dictionary): String {
|
||||
return stringify(d)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetDictionary(): Dictionary {
|
||||
var d = Dictionary()
|
||||
d.putAll(mapOf("a" to 1, "b" to 2))
|
||||
return d
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetDictionaryArray(): Array<Dictionary> {
|
||||
var d = Dictionary()
|
||||
d.putAll(mapOf("a" to 1, "b" to 2))
|
||||
return arrayOf(d)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testMethod(int: Int, array: IntArray): String {
|
||||
return "IntArray: " + array.contentToString()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testMethod(int: Int, vararg args: String): String {
|
||||
return "StringArray: " + args.contentToString()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testMethod(int: Int, objects: Array<TestClass2>): String {
|
||||
return "testObjects: " + objects.joinToString(separator = " ") { it.getValue().toString() }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testExc(i: Int): Int {
|
||||
val s: String? = null
|
||||
s!!.length
|
||||
return i
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testArgLong(a: Long): String {
|
||||
return "${a}"
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testArgBoolArray(a: BooleanArray): String {
|
||||
return a.contentToString();
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testArgByteArray(a: ByteArray): String {
|
||||
return a.contentToString();
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testArgCharArray(a: CharArray): String {
|
||||
return a.joinToString("")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testArgShortArray(a: ShortArray): String {
|
||||
return a.contentToString();
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testArgIntArray(a: IntArray): String {
|
||||
return a.contentToString();
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testArgLongArray(a: LongArray): String {
|
||||
return a.contentToString();
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testArgFloatArray(a: FloatArray): String {
|
||||
return a.contentToString();
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testArgDoubleArray(a: DoubleArray): String {
|
||||
return a.contentToString();
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetBoolArray(): BooleanArray {
|
||||
return booleanArrayOf(true, false, true)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetByteArray(): ByteArray {
|
||||
return byteArrayOf(1, 2, 3)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetCharArray(): CharArray {
|
||||
return "abc".toCharArray()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetShortArray(): ShortArray {
|
||||
return shortArrayOf(11, 12, 13)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetIntArray(): IntArray {
|
||||
return intArrayOf(21, 22, 23)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetLongArray(): LongArray {
|
||||
return longArrayOf(41, 42, 43)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetFloatArray(): FloatArray {
|
||||
return floatArrayOf(31.1f, 32.2f, 33.3f)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetDoubleArray(): DoubleArray {
|
||||
return doubleArrayOf(41.1, 42.2, 43.3)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetWrappedBoolArray(): Array<Boolean> {
|
||||
return arrayOf(true, false, true)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetWrappedByteArray(): Array<Byte> {
|
||||
return arrayOf(1, 2, 3)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetWrappedCharArray(): Array<Char> {
|
||||
return arrayOf('a', 'b', 'c')
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetWrappedShortArray(): Array<Short> {
|
||||
return arrayOf(11, 12, 13)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetWrappedIntArray(): Array<Int> {
|
||||
return arrayOf(21, 22, 23)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetWrappedLongArray(): Array<Long> {
|
||||
return arrayOf(41, 42, 43)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetWrappedFloatArray(): Array<Float> {
|
||||
return arrayOf(31.1f, 32.2f, 33.3f)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetWrappedDoubleArray(): Array<Double> {
|
||||
return arrayOf(41.1, 42.2, 43.3)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetObjectArray(): Array<TestClass2> {
|
||||
return arrayOf(TestClass2(51), TestClass2(52));
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetStringArray(): Array<String> {
|
||||
return arrayOf("I", "am", "String")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testRetCharSequenceArray(): Array<CharSequence> {
|
||||
return arrayOf("I", "am", "CharSequence")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testObjectOverload(a: TestClass2): String {
|
||||
return "TestClass2: $a"
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testObjectOverload(a: TestClass3): String {
|
||||
return "TestClass3: $a"
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testObjectOverloadArray(a: Array<TestClass2>): String {
|
||||
return "TestClass2: " + a.contentToString()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testObjectOverloadArray(a: Array<TestClass3>): String {
|
||||
return "TestClass3: " + a.contentToString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/**************************************************************************/
|
||||
/* TestClass2.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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 com.godot.game.test.javaclasswrapper
|
||||
|
||||
class TestClass2(private val value: Int) {
|
||||
fun getValue(): Int {
|
||||
return value
|
||||
}
|
||||
override fun toString(): String {
|
||||
return value.toString()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/**************************************************************************/
|
||||
/* TestClass3.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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 com.godot.game.test.javaclasswrapper
|
||||
|
||||
class TestClass3(private val value: String) {
|
||||
fun getValue(): String {
|
||||
return value
|
||||
}
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="godot_project_name_string">Godot App Instrumented Tests</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0"
|
||||
android:installLocation="auto" >
|
||||
|
||||
<supports-screens
|
||||
android:smallScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:largeScreens="true"
|
||||
android:xlargeScreens="true" />
|
||||
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00030000"
|
||||
android:required="true" />
|
||||
|
||||
<application
|
||||
android:label="@string/godot_project_name_string"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/icon"
|
||||
android:appCategory="game"
|
||||
android:isGame="true"
|
||||
android:hasFragileUserData="false"
|
||||
android:requestLegacyExternalStorage="false"
|
||||
tools:ignore="GoogleAppIndexingWarning" >
|
||||
<profileable
|
||||
android:shell="true"
|
||||
android:enabled="true"
|
||||
tools:targetApi="29" />
|
||||
|
||||
<activity
|
||||
android:name=".GodotApp"
|
||||
android:theme="@style/GodotAppSplashTheme"
|
||||
android:launchMode="singleInstancePerTask"
|
||||
android:excludeFromRecents="false"
|
||||
android:exported="false"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:screenOrientation="landscape"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:resizeableActivity="false"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
<activity-alias
|
||||
android:name=".GodotAppLauncher"
|
||||
android:targetActivity=".GodotApp"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
2
engine/platform/android/java/app/src/main/assets/.gitignore
vendored
Normal file
2
engine/platform/android/java/app/src/main/assets/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
/**************************************************************************/
|
||||
/* GodotApp.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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 com.godot.game;
|
||||
|
||||
import org.godotengine.godot.Godot;
|
||||
import org.godotengine.godot.GodotActivity;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.activity.EdgeToEdge;
|
||||
import androidx.core.splashscreen.SplashScreen;
|
||||
|
||||
/**
|
||||
* Template activity for Godot Android builds.
|
||||
* Feel free to extend and modify this class for your custom logic.
|
||||
*/
|
||||
public class GodotApp extends GodotActivity {
|
||||
static {
|
||||
// .NET libraries.
|
||||
if (BuildConfig.FLAVOR.equals("mono")) {
|
||||
try {
|
||||
Log.v("GODOT", "Loading System.Security.Cryptography.Native.Android library");
|
||||
System.loadLibrary("System.Security.Cryptography.Native.Android");
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
Log.e("GODOT", "Unable to load System.Security.Cryptography.Native.Android library");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final Runnable updateWindowAppearance = () -> {
|
||||
Godot godot = getGodot();
|
||||
if (godot != null) {
|
||||
godot.enableImmersiveMode(godot.isInImmersiveMode(), true);
|
||||
godot.enableEdgeToEdge(godot.isInEdgeToEdgeMode(), true);
|
||||
godot.setSystemBarsAppearance();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
SplashScreen.installSplashScreen(this);
|
||||
EdgeToEdge.enable(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updateWindowAppearance.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGodotMainLoopStarted() {
|
||||
super.onGodotMainLoopStarted();
|
||||
runOnUiThread(updateWindowAppearance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGodotForceQuit(Godot instance) {
|
||||
if (!BuildConfig.FLAVOR.equals("instrumented")) {
|
||||
// For instrumented builds, we disable force-quitting to allow the instrumented tests to complete
|
||||
// successfully, otherwise they fail when the process crashes.
|
||||
super.onGodotForceQuit(instance);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isPiPEnabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
331
engine/platform/android/java/build.gradle
Normal file
331
engine/platform/android/java/build.gradle
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
plugins {
|
||||
id 'io.github.gradle-nexus.publish-plugin'
|
||||
}
|
||||
|
||||
apply from: 'app/config.gradle'
|
||||
apply from: 'scripts/publish-root.gradle'
|
||||
|
||||
ext {
|
||||
PUBLISH_VERSION = getGodotPublishVersion()
|
||||
}
|
||||
|
||||
group = ossrhGroupId
|
||||
version = PUBLISH_VERSION
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
maven { url "https://central.sonatype.com/repository/maven-snapshots/"}
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
supportedAbis = ["arm32", "arm64", "x86_32", "x86_64"]
|
||||
supportedFlavors = ["editor", "template"]
|
||||
supportedAndroidDistributions = ["android", "horizonos", "picoos"]
|
||||
supportedFlavorsBuildTypes = [
|
||||
"editor": ["debug", "release"],
|
||||
"template": ["debug", "release"]
|
||||
]
|
||||
supportedEditions = ["standard", "mono"]
|
||||
|
||||
// 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
|
||||
// `./gradlew generateGodotTemplates` build command instead after running the `scons` command(s).
|
||||
// The {selectedAbis} values must be from the {supportedAbis} values.
|
||||
selectedAbis = ["arm64"]
|
||||
|
||||
rootDir = "../../.."
|
||||
binDir = "$rootDir/bin/"
|
||||
androidEditorBuildsDir = "$binDir/android_editor_builds/"
|
||||
}
|
||||
|
||||
def getSconsTaskName(String flavor, String buildType, String abi) {
|
||||
return "compileGodotNativeLibs" + flavor.capitalize() + buildType.capitalize() + abi.capitalize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Godot gradle build template by zipping the source files from the app directory, as well
|
||||
* as the AAR files generated by 'copyDebugAAR' and 'copyReleaseAAR'.
|
||||
* The zip file also includes some gradle tools to enable gradle builds from the Godot Editor.
|
||||
*/
|
||||
task zipGradleBuild(type: Zip) {
|
||||
onlyIf { generateGodotTemplates.state.executed || generateGodotMonoTemplates.state.executed }
|
||||
doFirst {
|
||||
logger.lifecycle("Generating Godot gradle build template")
|
||||
}
|
||||
from(fileTree(dir: 'app', excludes: ['**/build/**', '**/.gradle/**', '**/*.iml']), fileTree(dir: '.', includes: ['gradlew', 'gradlew.bat', 'gradle/**']))
|
||||
include '**/*'
|
||||
archiveFileName = 'android_source.zip'
|
||||
destinationDirectory = file(binDir)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the scons build tasks responsible for generating the Godot native shared
|
||||
* libraries should be excluded.
|
||||
*/
|
||||
def excludeSconsBuildTasks() {
|
||||
return !isAndroidStudio() && !project.hasProperty("generateNativeLibs")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the list of build tasks that should be excluded from the build process.\
|
||||
*/
|
||||
def templateExcludedBuildTask() {
|
||||
// We exclude these gradle tasks so we can run the scons command manually.
|
||||
def excludedTasks = []
|
||||
if (excludeSconsBuildTasks()) {
|
||||
logger.info("Excluding Android studio build tasks")
|
||||
for (String flavor : supportedFlavors) {
|
||||
String[] supportedBuildTypes = supportedFlavorsBuildTypes[flavor]
|
||||
for (String buildType : supportedBuildTypes) {
|
||||
for (String abi : selectedAbis) {
|
||||
excludedTasks += ":lib:" + getSconsTaskName(flavor, buildType, abi)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return excludedTasks
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the build tasks for the given flavor
|
||||
* @param flavor Must be one of the supported flavors ('template' / 'editor')
|
||||
* @param edition Must be one of the supported editions ('standard' / 'mono')
|
||||
* @param androidDistro Must be one of the supported Android distributions ('android' / 'horizonos' / 'picoos')
|
||||
*/
|
||||
def generateBuildTasks(String flavor = "template", String edition = "standard", String androidDistro = "android") {
|
||||
if (!supportedFlavors.contains(flavor)) {
|
||||
throw new GradleException("Invalid build flavor: $flavor")
|
||||
}
|
||||
if (!supportedAndroidDistributions.contains(androidDistro)) {
|
||||
throw new GradleException("Invalid Android distribution: $androidDistro")
|
||||
}
|
||||
if (!supportedEditions.contains(edition)) {
|
||||
throw new GradleException("Invalid build edition: $edition")
|
||||
}
|
||||
if (edition == "mono" && flavor != "template") {
|
||||
throw new GradleException("'mono' edition only supports the 'template' flavor.")
|
||||
}
|
||||
|
||||
String capitalizedAndroidDistro = androidDistro.capitalize()
|
||||
def buildTasks = []
|
||||
|
||||
// Only build the binary files for which we have native shared libraries unless we intend
|
||||
// to run the scons build tasks.
|
||||
boolean excludeSconsBuildTasks = excludeSconsBuildTasks()
|
||||
boolean isTemplate = flavor == "template"
|
||||
String libsDir = isTemplate ? "lib/libs/" : "lib/libs/tools/"
|
||||
for (String target : supportedFlavorsBuildTypes[flavor]) {
|
||||
File targetLibs = new File(libsDir + target)
|
||||
|
||||
String targetSuffix = target
|
||||
|
||||
if (!excludeSconsBuildTasks || (targetLibs != null
|
||||
&& targetLibs.isDirectory()
|
||||
&& targetLibs.listFiles() != null
|
||||
&& targetLibs.listFiles().length > 0)) {
|
||||
|
||||
String capitalizedTarget = target.capitalize()
|
||||
String capitalizedEdition = edition.capitalize()
|
||||
if (isTemplate) {
|
||||
// Copy the Godot android library archive file into the app module libs directory.
|
||||
// Depends on the library build task to ensure the AAR file is generated prior to copying.
|
||||
String copyAARTaskName = "copy${capitalizedTarget}AARToAppModule"
|
||||
if (tasks.findByName(copyAARTaskName) != null) {
|
||||
buildTasks += tasks.getByName(copyAARTaskName)
|
||||
} else {
|
||||
buildTasks += tasks.create(name: copyAARTaskName, type: Copy) {
|
||||
dependsOn ":lib:assembleTemplate${capitalizedTarget}"
|
||||
from('lib/build/outputs/aar')
|
||||
include("godot-lib.template_${targetSuffix}.aar")
|
||||
into("app/libs/${target}")
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the Godot android library archive file into the root bin directory.
|
||||
// Depends on the library build task to ensure the AAR file is generated prior to copying.
|
||||
String copyAARToBinTaskName = "copy${capitalizedTarget}AARToBin"
|
||||
if (tasks.findByName(copyAARToBinTaskName) != null) {
|
||||
buildTasks += tasks.getByName(copyAARToBinTaskName)
|
||||
} else {
|
||||
buildTasks += tasks.create(name: copyAARToBinTaskName, type: Copy) {
|
||||
dependsOn ":lib:assembleTemplate${capitalizedTarget}"
|
||||
from('lib/build/outputs/aar')
|
||||
include("godot-lib.template_${targetSuffix}.aar")
|
||||
into(binDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the generated binary template into the Godot bin directory.
|
||||
// Depends on the app build task to ensure the binary is generated prior to copying.
|
||||
String copyBinaryTaskName = "copy${capitalizedEdition}${capitalizedTarget}BinaryToBin"
|
||||
if (tasks.findByName(copyBinaryTaskName) != null) {
|
||||
buildTasks += tasks.getByName(copyBinaryTaskName)
|
||||
} else {
|
||||
buildTasks += tasks.create(name: copyBinaryTaskName, type: Copy) {
|
||||
String filenameSuffix = edition == "mono" ? "${edition}${capitalizedTarget}" : target
|
||||
dependsOn ":app:assemble${capitalizedEdition}${capitalizedTarget}"
|
||||
from("app/build/outputs/apk/${edition}/${target}") {
|
||||
include("android_${filenameSuffix}.apk")
|
||||
}
|
||||
into(binDir)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Copy the generated editor apk to the bin directory.
|
||||
String copyEditorApkTaskName = "copyEditor${capitalizedAndroidDistro}${capitalizedTarget}ApkToBin"
|
||||
if (tasks.findByName(copyEditorApkTaskName) != null) {
|
||||
buildTasks += tasks.getByName(copyEditorApkTaskName)
|
||||
} else {
|
||||
buildTasks += tasks.create(name: copyEditorApkTaskName, type: Copy) {
|
||||
dependsOn ":editor:assemble${capitalizedAndroidDistro}${capitalizedTarget}"
|
||||
from("editor/build/outputs/apk/${androidDistro}/${target}") {
|
||||
include("android_editor-${androidDistro}-${target}*.apk")
|
||||
}
|
||||
into(androidEditorBuildsDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the generated editor aab to the bin directory.
|
||||
String copyEditorAabTaskName = "copyEditor${capitalizedAndroidDistro}${capitalizedTarget}AabToBin"
|
||||
if (tasks.findByName(copyEditorAabTaskName) != null) {
|
||||
buildTasks += tasks.getByName(copyEditorAabTaskName)
|
||||
} else {
|
||||
buildTasks += tasks.create(name: copyEditorAabTaskName, type: Copy) {
|
||||
dependsOn ":editor:bundle${capitalizedAndroidDistro}${capitalizedTarget}"
|
||||
from("editor/build/outputs/bundle/${androidDistro}${capitalizedTarget}")
|
||||
into(androidEditorBuildsDir)
|
||||
include("android_editor-${androidDistro}-${target}*.aab")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info("No native shared libs for target $target. Skipping build.")
|
||||
}
|
||||
}
|
||||
|
||||
return buildTasks
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the Godot Editor binaries for Android devices.
|
||||
*
|
||||
* Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries
|
||||
* must have been generated (via scons) prior to running this gradle task.
|
||||
* The task will only build the binaries for which the shared libraries is available.
|
||||
*/
|
||||
task generateGodotEditor {
|
||||
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
|
||||
dependsOn = generateBuildTasks("editor", "standard", "android")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the Godot Editor binaries for HorizonOS devices.
|
||||
*
|
||||
* Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries
|
||||
* must have been generated (via scons) prior to running this gradle task.
|
||||
* The task will only build the binaries for which the shared libraries is available.
|
||||
*/
|
||||
task generateGodotHorizonOSEditor {
|
||||
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
|
||||
dependsOn = generateBuildTasks("editor", "standard", "horizonos")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the Godot Editor binaries for PicoOS devices.
|
||||
*
|
||||
* Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries
|
||||
* must have been generated (via scons) prior to running this gradle task.
|
||||
* The task will only build the binaries for which the shared libraries is available.
|
||||
*/
|
||||
task generateGodotPicoOSEditor {
|
||||
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
|
||||
dependsOn = generateBuildTasks("editor", "standard", "picoos")
|
||||
}
|
||||
|
||||
/**
|
||||
* Master task used to coordinate the tasks defined above to generate the set of Godot templates.
|
||||
*/
|
||||
task generateGodotTemplates {
|
||||
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
|
||||
dependsOn = generateBuildTasks("template")
|
||||
|
||||
finalizedBy 'zipGradleBuild'
|
||||
}
|
||||
|
||||
/**
|
||||
* Master task used to coordinate the tasks defined above to generate the set of Godot templates
|
||||
* for the 'mono' edition of the engine.
|
||||
*/
|
||||
task generateGodotMonoTemplates {
|
||||
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
|
||||
dependsOn = generateBuildTasks("template", "mono")
|
||||
|
||||
finalizedBy 'zipGradleBuild'
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
dependsOn 'cleanGodotEditor'
|
||||
dependsOn 'cleanGodotTemplates'
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 generated aab binaries
|
||||
delete("editor/build/outputs/bundle")
|
||||
|
||||
// Delete the Godot editor apks & aabs in the Godot bin directory
|
||||
delete(androidEditorBuildsDir)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean the generated template artifacts.
|
||||
*/
|
||||
task cleanGodotTemplates(type: Delete) {
|
||||
// Delete the generated native libs
|
||||
delete("lib/libs")
|
||||
|
||||
// Delete the library generated AAR files
|
||||
delete("lib/build/outputs/aar")
|
||||
|
||||
// Delete the app libs directory contents
|
||||
delete("app/libs")
|
||||
|
||||
// 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_monoDebug.apk")
|
||||
delete("$binDir/android_monoRelease.apk")
|
||||
delete("$binDir/android_source.zip")
|
||||
delete("$binDir/godot-lib.template_debug.aar")
|
||||
delete("$binDir/godot-lib.template_release.aar")
|
||||
|
||||
// Cover deletion for the libs using the previous naming scheme
|
||||
delete("$binDir/godot-lib.debug.aar")
|
||||
delete("$binDir/godot-lib.release.aar")
|
||||
|
||||
// Delete the native debug symbols files.
|
||||
delete("$binDir/android-editor-debug-native-symbols.zip")
|
||||
delete("$binDir/android-editor-release-native-symbols.zip")
|
||||
delete("$binDir/android-template-debug-native-symbols.zip")
|
||||
delete("$binDir/android-template-release-native-symbols.zip")
|
||||
}
|
||||
245
engine/platform/android/java/editor/build.gradle
Normal file
245
engine/platform/android/java/editor/build.gradle
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
// Gradle build config for Godot Engine's Android port.
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'base'
|
||||
}
|
||||
|
||||
ext {
|
||||
// Retrieve the build number from the environment variable; default to 0 if none is specified.
|
||||
// The build number is added as a suffix to the version code for upload to the Google Play store.
|
||||
getEditorBuildNumber = { ->
|
||||
int buildNumber = 0
|
||||
String versionStatus = System.getenv("GODOT_VERSION_STATUS")
|
||||
if (versionStatus != null && !versionStatus.isEmpty()) {
|
||||
try {
|
||||
buildNumber = Integer.parseInt(versionStatus.replaceAll("[^0-9]", ""))
|
||||
} catch (NumberFormatException ignored) {
|
||||
buildNumber = 0
|
||||
}
|
||||
}
|
||||
|
||||
return buildNumber
|
||||
}
|
||||
// Value by which the Godot version code should be offset by to make room for the build number
|
||||
editorBuildNumberOffset = 100
|
||||
|
||||
// Return the keystore file used for signing the release build.
|
||||
getGodotKeystoreFile = { ->
|
||||
def keyStore = System.getenv("GODOT_ANDROID_SIGN_KEYSTORE")
|
||||
if (keyStore == null || keyStore.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
return file(keyStore)
|
||||
}
|
||||
|
||||
// Return the key alias used for signing the release build.
|
||||
getGodotKeyAlias = { ->
|
||||
def kAlias = System.getenv("GODOT_ANDROID_KEYSTORE_ALIAS")
|
||||
return kAlias
|
||||
}
|
||||
|
||||
// Return the password for the key used for signing the release build.
|
||||
getGodotSigningPassword = { ->
|
||||
def signingPassword = System.getenv("GODOT_ANDROID_SIGN_PASSWORD")
|
||||
return signingPassword
|
||||
}
|
||||
|
||||
// Returns true if the environment variables contains the configuration for signing the release
|
||||
// build.
|
||||
hasReleaseSigningConfigs = { ->
|
||||
def keystoreFile = getGodotKeystoreFile()
|
||||
def keyAlias = getGodotKeyAlias()
|
||||
def signingPassword = getGodotSigningPassword()
|
||||
|
||||
return keystoreFile != null && keystoreFile.isFile()
|
||||
&& keyAlias != null && !keyAlias.isEmpty()
|
||||
&& signingPassword != null && !signingPassword.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
def generateVersionCode() {
|
||||
int libraryVersionCode = getGodotLibraryVersionCode()
|
||||
return (libraryVersionCode * editorBuildNumberOffset) + getEditorBuildNumber()
|
||||
}
|
||||
|
||||
def generateVersionName() {
|
||||
String libraryVersionName = getGodotLibraryVersionName()
|
||||
int buildNumber = getEditorBuildNumber()
|
||||
return buildNumber == 0 ? libraryVersionName : libraryVersionName + ".$buildNumber"
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
buildToolsVersion versions.buildTools
|
||||
ndkVersion versions.ndkVersion
|
||||
|
||||
namespace = "org.godotengine.editor"
|
||||
|
||||
defaultConfig {
|
||||
// The 'applicationId' suffix allows to install Godot 3.x(v3) and 4.x(v4) on the same device
|
||||
applicationId "org.godotengine.editor.v4"
|
||||
versionCode generateVersionCode()
|
||||
versionName generateVersionName()
|
||||
minSdkVersion versions.minSdk
|
||||
targetSdkVersion versions.targetSdk
|
||||
|
||||
missingDimensionStrategy 'products', 'editor'
|
||||
manifestPlaceholders += [
|
||||
editorAppName: "Godot Engine 4",
|
||||
editorBuildSuffix: ""
|
||||
]
|
||||
|
||||
ndk { debugSymbolLevel 'NONE' }
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// The following argument makes the Android Test Orchestrator run its
|
||||
// "pm clear" command after each test invocation. This command ensures
|
||||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
}
|
||||
|
||||
base {
|
||||
archivesName = "android_editor"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility versions.javaVersion
|
||||
targetCompatibility versions.javaVersion
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = versions.javaVersion
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
storeFile getGodotKeystoreFile()
|
||||
storePassword getGodotSigningPassword()
|
||||
keyAlias getGodotKeyAlias()
|
||||
keyPassword getGodotSigningPassword()
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
manifestPlaceholders += [editorBuildSuffix: " (debug)"]
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
|
||||
release {
|
||||
if (hasReleaseSigningConfigs()) {
|
||||
signingConfig signingConfigs.release
|
||||
} else {
|
||||
// We default to the debug signingConfigs when the release signing configs are not
|
||||
// available (e.g: development in Android Studio).
|
||||
signingConfig signingConfigs.debug
|
||||
// In addition, we update the application ID to allow installing an Android studio release build
|
||||
// side by side with a production build from the store.
|
||||
applicationIdSuffix ".release"
|
||||
manifestPlaceholders += [editorBuildSuffix: " (release)"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
// Debug symbols are kept for development within Android Studio.
|
||||
if (shouldNotStrip()) {
|
||||
jniLibs {
|
||||
keepDebugSymbols += '**/*.so'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions = ["android_distribution"]
|
||||
productFlavors {
|
||||
android {
|
||||
dimension "android_distribution"
|
||||
missingDimensionStrategy 'products', 'editor'
|
||||
}
|
||||
horizonos {
|
||||
dimension "android_distribution"
|
||||
missingDimensionStrategy 'products', 'editor'
|
||||
ndk {
|
||||
//noinspection ChromeOsAbiSupport
|
||||
abiFilters "arm64-v8a"
|
||||
}
|
||||
applicationIdSuffix ".meta"
|
||||
versionNameSuffix "-meta"
|
||||
targetSdkVersion 32
|
||||
}
|
||||
picoos {
|
||||
dimension "android_distribution"
|
||||
missingDimensionStrategy 'products', 'editor'
|
||||
ndk {
|
||||
//noinspection ChromeOsAbiSupport
|
||||
abiFilters "arm64-v8a"
|
||||
}
|
||||
applicationIdSuffix ".pico"
|
||||
versionNameSuffix "-pico"
|
||||
minSdkVersion 29
|
||||
targetSdkVersion 32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
|
||||
|
||||
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
|
||||
implementation project(":lib")
|
||||
|
||||
implementation "androidx.window:window:1.3.0"
|
||||
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.2.1"
|
||||
implementation "org.bouncycastle:bcprov-jdk15to18:1.78"
|
||||
|
||||
implementation "org.khronos.openxr:openxr_loader_for_android:$versions.openxrLoaderVersion"
|
||||
|
||||
// Android XR dependencies
|
||||
androidImplementation "org.godotengine:godot-openxr-vendors-androidxr:$versions.openxrVendorsVersion"
|
||||
// Meta dependencies
|
||||
horizonosImplementation "org.godotengine:godot-openxr-vendors-meta:$versions.openxrVendorsVersion"
|
||||
// Pico dependencies
|
||||
picoosImplementation "org.godotengine:godot-openxr-vendors-pico:$versions.openxrVendorsVersion"
|
||||
|
||||
// Android instrumented test dependencies
|
||||
androidTestImplementation "androidx.test.ext:junit:$versions.junitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$versions.espressoCoreVersion"
|
||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$versions.kotlinTestVersion"
|
||||
androidTestImplementation "androidx.test:runner:$versions.testRunnerVersion"
|
||||
androidTestUtil "androidx.test:orchestrator:$versions.testOrchestratorVersion"
|
||||
}
|
||||
|
||||
/*
|
||||
* Older versions of our vendor plugin include a loader that we no longer need.
|
||||
* This code ensures those are removed.
|
||||
*/
|
||||
tasks.withType( com.android.build.gradle.internal.tasks.MergeNativeLibsTask) {
|
||||
doFirst {
|
||||
externalLibNativeLibs.each { jniDir ->
|
||||
if (jniDir.getCanonicalPath().contains("godot-openxr-") || jniDir.getCanonicalPath().contains("godotopenxr")) {
|
||||
// Delete the 'libopenxr_loader.so' files from the vendors plugin so we only use the version from the
|
||||
// openxr loader dependency.
|
||||
File armFile = new File(jniDir, "arm64-v8a/libopenxr_loader.so")
|
||||
if (armFile.exists()) {
|
||||
armFile.delete()
|
||||
}
|
||||
File x86File = new File(jniDir, "x86_64/libopenxr_loader.so")
|
||||
if (x86File.exists()) {
|
||||
x86File.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
engine/platform/android/java/editor/src/.gitignore
vendored
Normal file
1
engine/platform/android/java/editor/src/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
!/debug
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.xr.api.openxr"
|
||||
android:required="false" />
|
||||
|
||||
<!-- 6dof motion controllers -->
|
||||
<uses-feature android:name="android.hardware.xr.input.controller" android:required="false" />
|
||||
|
||||
<!-- Eye tracking -->
|
||||
<uses-feature android:name="android.hardware.xr.input.eye_tracking" android:required="false" />
|
||||
<uses-permission android:name="android.permission.EYE_TRACKING_FINE" />
|
||||
|
||||
<!-- Hand tracking -->
|
||||
<uses-feature android:name="android.hardware.xr.input.hand_tracking" android:required="false" />
|
||||
<uses-permission android:name="android.permission.HAND_TRACKING" />
|
||||
|
||||
<application>
|
||||
<uses-native-library android:name="libopenxr.google.so" android:required="false" />
|
||||
|
||||
<property
|
||||
android:name="android.window.PROPERTY_XR_BOUNDARY_TYPE_RECOMMENDED"
|
||||
android:value="XR_BOUNDARY_TYPE_NO_RECOMMENDATION" />
|
||||
|
||||
<activity
|
||||
android:name=".GodotXRGame"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" />
|
||||
</intent-filter>
|
||||
<property
|
||||
android:name="android.window.PROPERTY_XR_ACTIVITY_START_MODE"
|
||||
android:value="XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED" />
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/**************************************************************************/
|
||||
/* GodotEditor.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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
|
||||
|
||||
/**
|
||||
* Primary window of the Godot Editor.
|
||||
*
|
||||
* This is the implementation of the editor used when running on Android devices.
|
||||
*/
|
||||
open class GodotEditor : BaseGodotEditor() {
|
||||
|
||||
override fun getXRRuntimePermissions(): MutableSet<String> {
|
||||
val xrRuntimePermissions = super.getXRRuntimePermissions()
|
||||
|
||||
xrRuntimePermissions.add("android.permission.EYE_TRACKING_FINE")
|
||||
xrRuntimePermissions.add("android.permission.HAND_TRACKING")
|
||||
|
||||
return xrRuntimePermissions
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
/**************************************************************************/
|
||||
/* GodotEditorTest.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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 android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.godotengine.godot.GodotActivity.Companion.EXTRA_COMMAND_LINE_PARAMS
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Instrumented test for the Godot editor.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class GodotEditorTest {
|
||||
companion object {
|
||||
private val TAG = GodotEditorTest::class.simpleName
|
||||
|
||||
private val TEST_COMMAND_LINE_PARAMS = arrayOf("This is a test")
|
||||
private const val PROJECT_MANAGER_CLASS_NAME = "org.godotengine.editor.ProjectManager"
|
||||
private const val GODOT_EDITOR_CLASS_NAME = "org.godotengine.editor.GodotEditor"
|
||||
}
|
||||
|
||||
/**
|
||||
* Implicitly launch the project manager.
|
||||
*/
|
||||
@Test
|
||||
fun testImplicitProjectManagerLaunch() {
|
||||
val implicitLaunchIntent = Intent().apply {
|
||||
setPackage(BuildConfig.APPLICATION_ID)
|
||||
action = Intent.ACTION_MAIN
|
||||
addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
putExtra(EXTRA_COMMAND_LINE_PARAMS, TEST_COMMAND_LINE_PARAMS)
|
||||
}
|
||||
ActivityScenario.launch<GodotEditor>(implicitLaunchIntent).use { scenario ->
|
||||
scenario.onActivity { activity ->
|
||||
assertEquals(activity.intent.component?.className, PROJECT_MANAGER_CLASS_NAME)
|
||||
|
||||
val commandLineParams = activity.intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
|
||||
assertNull(commandLineParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly launch the project manager.
|
||||
*/
|
||||
@Test
|
||||
fun testExplicitProjectManagerLaunch() {
|
||||
val explicitProjectManagerIntent = Intent().apply {
|
||||
component = ComponentName(BuildConfig.APPLICATION_ID, PROJECT_MANAGER_CLASS_NAME)
|
||||
putExtra(EXTRA_COMMAND_LINE_PARAMS, TEST_COMMAND_LINE_PARAMS)
|
||||
}
|
||||
ActivityScenario.launch<GodotEditor>(explicitProjectManagerIntent).use { scenario ->
|
||||
scenario.onActivity { activity ->
|
||||
assertEquals(activity.intent.component?.className, PROJECT_MANAGER_CLASS_NAME)
|
||||
|
||||
val commandLineParams = activity.intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
|
||||
assertNull(commandLineParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly launch the `GodotEditor` activity.
|
||||
*/
|
||||
@Test
|
||||
fun testExplicitGodotEditorLaunch() {
|
||||
val godotEditorIntent = Intent().apply {
|
||||
component = ComponentName(BuildConfig.APPLICATION_ID, GODOT_EDITOR_CLASS_NAME)
|
||||
putExtra(EXTRA_COMMAND_LINE_PARAMS, TEST_COMMAND_LINE_PARAMS)
|
||||
}
|
||||
ActivityScenario.launch<GodotEditor>(godotEditorIntent).use { scenario ->
|
||||
scenario.onActivity { activity ->
|
||||
assertEquals(activity.intent.component?.className, GODOT_EDITOR_CLASS_NAME)
|
||||
|
||||
val commandLineParams = activity.intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
|
||||
assertNotNull(commandLineParams)
|
||||
assertTrue(commandLineParams.contentEquals(TEST_COMMAND_LINE_PARAMS))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:horizonos="http://schemas.horizonos/sdk">
|
||||
|
||||
<horizonos:uses-horizonos-sdk
|
||||
horizonos:minSdkVersion="69"
|
||||
horizonos:targetSdkVersion="69" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.vr.headtracking"
|
||||
android:required="true"
|
||||
android:version="1"/>
|
||||
|
||||
<!-- Oculus Quest hand tracking -->
|
||||
<uses-permission android:name="com.oculus.permission.HAND_TRACKING" />
|
||||
<uses-feature
|
||||
android:name="oculus.software.handtracking"
|
||||
android:required="false" />
|
||||
|
||||
<!-- Passthrough feature flag -->
|
||||
<uses-feature android:name="com.oculus.feature.PASSTHROUGH"
|
||||
android:required="false" />
|
||||
|
||||
<!-- Overlay keyboard support -->
|
||||
<uses-feature android:name="oculus.software.overlay_keyboard" android:required="false"/>
|
||||
|
||||
<!-- Render model -->
|
||||
<uses-permission android:name="com.oculus.permission.RENDER_MODEL" />
|
||||
<uses-feature android:name="com.oculus.feature.RENDER_MODEL" android:required="false" />
|
||||
|
||||
<!-- Anchor api -->
|
||||
<uses-permission android:name="com.oculus.permission.USE_ANCHOR_API" />
|
||||
|
||||
<!-- Scene api -->
|
||||
<uses-permission android:name="com.oculus.permission.USE_SCENE" />
|
||||
|
||||
<!-- Temp removal of the 'REQUEST_INSTALL_PACKAGES' permission as it's currently forbidden by the Horizon OS store -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove" />
|
||||
|
||||
<!-- Passthrough feature -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera2.any"
|
||||
android:required="false"/>
|
||||
<uses-permission android:name="horizonos.permission.HEADSET_CAMERA"/>
|
||||
|
||||
<application>
|
||||
|
||||
<activity
|
||||
android:name=".GodotEditor"
|
||||
android:exported="false"
|
||||
android:screenOrientation="landscape"
|
||||
tools:node="merge"
|
||||
tools:replace="android:screenOrientation">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="com.oculus.intent.category.2D" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="com.oculus.vrshell.free_resizing_lock_aspect_ratio" android:value="true"/>
|
||||
</activity>
|
||||
<activity-alias
|
||||
android:name=".ProjectManager"
|
||||
android:exported="true"
|
||||
tools:node="merge"
|
||||
android:targetActivity=".GodotEditor">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="com.oculus.intent.category.2D" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
<activity
|
||||
android:name=".GodotXRGame"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="com.oculus.intent.category.VR" />
|
||||
<category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Supported Meta devices -->
|
||||
<meta-data
|
||||
android:name="com.oculus.supportedDevices"
|
||||
android:value="quest2|quest3|questpro|quest3s"
|
||||
tools:replace="android:value" />
|
||||
|
||||
<!-- Enable system splash screen -->
|
||||
<meta-data android:name="com.oculus.ossplash" android:value="true"/>
|
||||
<!-- Enable passthrough background during the splash screen -->
|
||||
<meta-data android:name="com.oculus.ossplash.background" android:value="passthrough-contextual"/>
|
||||
|
||||
<meta-data android:name="com.oculus.handtracking.version" android:value="V2.0" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -0,0 +1,56 @@
|
|||
/**************************************************************************/
|
||||
/* GodotEditor.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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
|
||||
|
||||
/**
|
||||
* Primary window of the Godot Editor.
|
||||
*
|
||||
* This is the implementation of the editor used when running on HorizonOS devices.
|
||||
*/
|
||||
open class GodotEditor : BaseGodotEditor() {
|
||||
|
||||
override fun getExcludedPermissions(): MutableSet<String> {
|
||||
val excludedPermissions = super.getExcludedPermissions().apply {
|
||||
// The AVATAR_CAMERA and HEADSET_CAMERA permissions are requested when `CameraFeed.feed_is_active`
|
||||
// is enabled.
|
||||
add("horizonos.permission.AVATAR_CAMERA")
|
||||
add("horizonos.permission.HEADSET_CAMERA")
|
||||
}
|
||||
return excludedPermissions
|
||||
}
|
||||
|
||||
override fun getXRRuntimePermissions(): MutableSet<String> {
|
||||
val xrRuntimePermissions = super.getXRRuntimePermissions()
|
||||
xrRuntimePermissions.add("com.oculus.permission.USE_SCENE")
|
||||
xrRuntimePermissions.add("horizonos.permission.USE_SCENE")
|
||||
return xrRuntimePermissions
|
||||
}
|
||||
}
|
||||
153
engine/platform/android/java/editor/src/main/AndroidManifest.xml
Normal file
153
engine/platform/android/java/editor/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="auto">
|
||||
|
||||
<queries>
|
||||
<package android:name="org.godotengine.godot_gradle_build_environment" />
|
||||
</queries>
|
||||
|
||||
<supports-screens
|
||||
android:largeScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:smallScreens="false"
|
||||
android:xlargeScreens="true" />
|
||||
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00030000"
|
||||
android:required="true" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/themed_icon"
|
||||
android:label="${editorAppName}${editorBuildSuffix}"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:theme="@style/GodotEditorSplashScreenTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<profileable
|
||||
android:shell="true"
|
||||
android:enabled="true"
|
||||
tools:targetApi="29" />
|
||||
|
||||
<activity
|
||||
android:name=".GodotEditor"
|
||||
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:exported="false"
|
||||
android:icon="@mipmap/themed_icon"
|
||||
android:launchMode="singleInstancePerTask"
|
||||
android:screenOrientation="userLandscape">
|
||||
<layout
|
||||
android:defaultWidth="@dimen/editor_default_window_width"
|
||||
android:defaultHeight="@dimen/editor_default_window_height" />
|
||||
|
||||
<!-- Intent filter used to intercept hybrid PANEL launch for the current editor project, and route it
|
||||
properly through the editor 'run' logic (e.g: debugger setup) -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="org.godotengine.xr.hybrid.PANEL" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Intent filter used to intercept hybrid IMMERSIVE launch for the current editor project, and route it
|
||||
properly through the editor 'run' logic (e.g: debugger setup) -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="org.godotengine.xr.hybrid.IMMERSIVE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity-alias
|
||||
android:name=".ProjectManager"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/themed_icon"
|
||||
android:targetActivity=".GodotEditor">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*\\.godot" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
<activity
|
||||
android:name=".GodotGame"
|
||||
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:exported="false"
|
||||
android:icon="@mipmap/ic_play_window"
|
||||
android:label="@string/godot_game_activity_name"
|
||||
android:launchMode="singleTask"
|
||||
android:process=":GodotGame"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:theme="@style/GodotGameTheme"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:screenOrientation="userLandscape">
|
||||
<layout
|
||||
android:defaultWidth="@dimen/editor_default_window_width"
|
||||
android:defaultHeight="@dimen/editor_default_window_height" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".embed.EmbeddedGodotGame"
|
||||
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:exported="false"
|
||||
android:icon="@mipmap/ic_play_window"
|
||||
android:label="@string/godot_game_activity_name"
|
||||
android:theme="@style/GodotEmbeddedGameTheme"
|
||||
android:taskAffinity=":embed"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:process=":EmbeddedGodotGame"
|
||||
android:supportsPictureInPicture="true" />
|
||||
<activity
|
||||
android:name=".GodotXRGame"
|
||||
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:process=":GodotXRGame"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=":xr"
|
||||
android:icon="@mipmap/ic_play_window"
|
||||
android:label="@string/godot_game_activity_name"
|
||||
android:exported="false"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:screenOrientation="landscape"
|
||||
android:resizeableActivity="false"
|
||||
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" />
|
||||
|
||||
<!--
|
||||
We remove this meta-data originating from the vendors plugin as we only need the loader for
|
||||
now since the project being edited provides its own version of the vendors plugin.
|
||||
|
||||
This needs to be removed once we start implementing the immersive version of the project
|
||||
manager and editor windows.
|
||||
-->
|
||||
<meta-data
|
||||
android:name="org.godotengine.plugin.v2.GodotOpenXR"
|
||||
android:value="org.godotengine.openxr.vendors.GodotOpenXR"
|
||||
tools:node="remove" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,550 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig;
|
||||
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.util.DataSink;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.util.RunnablesExecutor;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SignatureException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* APK signing logic which is independent of how input and output APKs are stored, parsed, and
|
||||
* generated.
|
||||
*
|
||||
* <p><h3>Operating Model</h3>
|
||||
*
|
||||
* The abstract operating model is that there is an input APK which is being signed, thus producing
|
||||
* an output APK. In reality, there may be just an output APK being built from scratch, or the input
|
||||
* APK and the output APK may be the same file. Because this engine does not deal with reading and
|
||||
* writing files, it can handle all of these scenarios.
|
||||
*
|
||||
* <p>The engine is stateful and thus cannot be used for signing multiple APKs. However, once
|
||||
* the engine signed an APK, the engine can be used to re-sign the APK after it has been modified.
|
||||
* This may be more efficient than signing the APK using a new instance of the engine. See
|
||||
* <a href="#incremental">Incremental Operation</a>.
|
||||
*
|
||||
* <p>In the engine's operating model, a signed APK is produced as follows.
|
||||
* <ol>
|
||||
* <li>JAR entries to be signed are output,</li>
|
||||
* <li>JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the
|
||||
* output,</li>
|
||||
* <li>JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature
|
||||
* to the output.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>The input APK may contain JAR entries which, depending on the engine's configuration, may or
|
||||
* may not be output (e.g., existing signatures may need to be preserved or stripped) or which the
|
||||
* engine will overwrite as part of signing. The engine thus offers {@link #inputJarEntry(String)}
|
||||
* which tells the client whether the input JAR entry needs to be output. This avoids the need for
|
||||
* the client to hard-code the aspects of APK signing which determine which parts of input must be
|
||||
* ignored. Similarly, the engine offers {@link #inputApkSigningBlock(DataSource)} to help the
|
||||
* client avoid dealing with preserving or stripping APK Signature Scheme v2 signature of the input
|
||||
* APK.
|
||||
*
|
||||
* <p>To use the engine to sign an input APK (or a collection of JAR entries), follow these
|
||||
* steps:
|
||||
* <ol>
|
||||
* <li>Obtain a new instance of the engine -- engine instances are stateful and thus cannot be used
|
||||
* for signing multiple APKs.</li>
|
||||
* <li>Locate the input APK's APK Signing Block and provide it to
|
||||
* {@link #inputApkSigningBlock(DataSource)}.</li>
|
||||
* <li>For each JAR entry in the input APK, invoke {@link #inputJarEntry(String)} to determine
|
||||
* whether this entry should be output. The engine may request to inspect the entry.</li>
|
||||
* <li>For each output JAR entry, invoke {@link #outputJarEntry(String)} which may request to
|
||||
* inspect the entry.</li>
|
||||
* <li>Once all JAR entries have been output, invoke {@link #outputJarEntries()} which may request
|
||||
* that additional JAR entries are output. These entries comprise the output APK's JAR
|
||||
* signature.</li>
|
||||
* <li>Locate the ZIP Central Directory and ZIP End of Central Directory sections in the output and
|
||||
* invoke {@link #outputZipSections2(DataSource, DataSource, DataSource)} which may request that
|
||||
* an APK Signature Block is inserted before the ZIP Central Directory. The block contains the
|
||||
* output APK's APK Signature Scheme v2 signature.</li>
|
||||
* <li>Invoke {@link #outputDone()} to signal that the APK was output in full. The engine will
|
||||
* confirm that the output APK is signed.</li>
|
||||
* <li>Invoke {@link #close()} to signal that the engine will no longer be used. This lets the
|
||||
* engine free any resources it no longer needs.
|
||||
* </ol>
|
||||
*
|
||||
* <p>Some invocations of the engine may provide the client with a task to perform. The client is
|
||||
* expected to perform all requested tasks before proceeding to the next stage of signing. See
|
||||
* documentation of each method about the deadlines for performing the tasks requested by the
|
||||
* method.
|
||||
*
|
||||
* <p><h3 id="incremental">Incremental Operation</h3></a>
|
||||
*
|
||||
* The engine supports incremental operation where a signed APK is produced, then modified and
|
||||
* re-signed. This may be useful for IDEs, where an app is frequently re-signed after small changes
|
||||
* by the developer. Re-signing may be more efficient than signing from scratch.
|
||||
*
|
||||
* <p>To use the engine in incremental mode, keep notifying the engine of changes to the APK through
|
||||
* {@link #inputApkSigningBlock(DataSource)}, {@link #inputJarEntry(String)},
|
||||
* {@link #inputJarEntryRemoved(String)}, {@link #outputJarEntry(String)},
|
||||
* and {@link #outputJarEntryRemoved(String)}, perform the tasks requested by the engine through
|
||||
* these methods, and, when a new signed APK is desired, run through steps 5 onwards to re-sign the
|
||||
* APK.
|
||||
*
|
||||
* <p><h3>Output-only Operation</h3>
|
||||
*
|
||||
* The engine's abstract operating model consists of an input APK and an output APK. However, it is
|
||||
* possible to use the engine in output-only mode where the engine's {@code input...} methods are
|
||||
* not invoked. In this mode, the engine has less control over output because it cannot request that
|
||||
* some JAR entries are not output. Nevertheless, the engine will attempt to make the output APK
|
||||
* signed and will report an error if cannot do so.
|
||||
*
|
||||
* @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
|
||||
*/
|
||||
public interface ApkSignerEngine extends Closeable {
|
||||
|
||||
default void setExecutor(RunnablesExecutor executor) {
|
||||
throw new UnsupportedOperationException("setExecutor method is not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the signer engine with the data already present in the apk (if any). There
|
||||
* might already be data that can be reused if the entries has not been changed.
|
||||
*
|
||||
* @param manifestBytes
|
||||
* @param entryNames
|
||||
* @return set of entry names which were processed by the engine during the initialization, a
|
||||
* subset of entryNames
|
||||
*/
|
||||
default Set<String> initWith(byte[] manifestBytes, Set<String> entryNames) {
|
||||
throw new UnsupportedOperationException("initWith method is not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the input APK contains the provided APK Signing Block. The
|
||||
* block may contain signatures of the input APK, such as APK Signature Scheme v2 signatures.
|
||||
*
|
||||
* @param apkSigningBlock APK signing block of the input APK. The provided data source is
|
||||
* guaranteed to not be used by the engine after this method terminates.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs while reading the APK Signing Block
|
||||
* @throws ApkFormatException if the APK Signing Block is malformed
|
||||
* @throws IllegalStateException if this engine is closed
|
||||
*/
|
||||
void inputApkSigningBlock(DataSource apkSigningBlock)
|
||||
throws IOException, ApkFormatException, IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the specified JAR entry was encountered in the input APK.
|
||||
*
|
||||
* <p>When an input entry is updated/changed, it's OK to not invoke
|
||||
* {@link #inputJarEntryRemoved(String)} before invoking this method.
|
||||
*
|
||||
* @return instructions about how to proceed with this entry
|
||||
*
|
||||
* @throws IllegalStateException if this engine is closed
|
||||
*/
|
||||
InputJarEntryInstructions inputJarEntry(String entryName) throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the specified JAR entry was output.
|
||||
*
|
||||
* <p>It is unnecessary to invoke this method for entries added to output by this engine (e.g.,
|
||||
* requested by {@link #outputJarEntries()}) provided the entries were output with exactly the
|
||||
* data requested by the engine.
|
||||
*
|
||||
* <p>When an already output entry is updated/changed, it's OK to not invoke
|
||||
* {@link #outputJarEntryRemoved(String)} before invoking this method.
|
||||
*
|
||||
* @return request to inspect the entry or {@code null} if the engine does not need to inspect
|
||||
* the entry. The request must be fulfilled before {@link #outputJarEntries()} is
|
||||
* invoked.
|
||||
*
|
||||
* @throws IllegalStateException if this engine is closed
|
||||
*/
|
||||
InspectJarEntryRequest outputJarEntry(String entryName) throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the specified JAR entry was removed from the input. It's safe
|
||||
* to invoke this for entries for which {@link #inputJarEntry(String)} hasn't been invoked.
|
||||
*
|
||||
* @return output policy of this JAR entry. The policy indicates how this input entry affects
|
||||
* the output APK. The client of this engine should use this information to determine
|
||||
* how the removal of this input APK's JAR entry affects the output APK.
|
||||
*
|
||||
* @throws IllegalStateException if this engine is closed
|
||||
*/
|
||||
InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName)
|
||||
throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the specified JAR entry was removed from the output. It's safe
|
||||
* to invoke this for entries for which {@link #outputJarEntry(String)} hasn't been invoked.
|
||||
*
|
||||
* @throws IllegalStateException if this engine is closed
|
||||
*/
|
||||
void outputJarEntryRemoved(String entryName) throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that all JAR entries have been output.
|
||||
*
|
||||
* @return request to add JAR signature to the output or {@code null} if there is no need to add
|
||||
* a JAR signature. The request will contain additional JAR entries to be output. The
|
||||
* request must be fulfilled before
|
||||
* {@link #outputZipSections2(DataSource, DataSource, DataSource)} is invoked.
|
||||
*
|
||||
* @throws ApkFormatException if the APK is malformed in a way which is preventing this engine
|
||||
* from producing a valid signature. For example, if the engine uses the provided
|
||||
* {@code META-INF/MANIFEST.MF} as a template and the file is malformed.
|
||||
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||
* cryptographic algorithm implementation is missing
|
||||
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||
* not suitable for generating the signature
|
||||
* @throws SignatureException if an error occurred while generating a signature
|
||||
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||
* entries, or if the engine is closed
|
||||
*/
|
||||
OutputJarSignatureRequest outputJarEntries()
|
||||
throws ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
|
||||
SignatureException, IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the ZIP sections comprising the output APK have been output.
|
||||
*
|
||||
* <p>The provided data sources are guaranteed to not be used by the engine after this method
|
||||
* terminates.
|
||||
*
|
||||
* @deprecated This is now superseded by {@link #outputZipSections2(DataSource, DataSource,
|
||||
* DataSource)}.
|
||||
*
|
||||
* @param zipEntries the section of ZIP archive containing Local File Header records and data of
|
||||
* the ZIP entries. In a well-formed archive, this section starts at the start of the
|
||||
* archive and extends all the way to the ZIP Central Directory.
|
||||
* @param zipCentralDirectory ZIP Central Directory section
|
||||
* @param zipEocd ZIP End of Central Directory (EoCD) record
|
||||
*
|
||||
* @return request to add an APK Signing Block to the output or {@code null} if the output must
|
||||
* not contain an APK Signing Block. The request must be fulfilled before
|
||||
* {@link #outputDone()} is invoked.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs while reading the provided ZIP sections
|
||||
* @throws ApkFormatException if the provided APK is malformed in a way which prevents this
|
||||
* engine from producing a valid signature. For example, if the APK Signing Block
|
||||
* provided to the engine is malformed.
|
||||
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||
* cryptographic algorithm implementation is missing
|
||||
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||
* not suitable for generating the signature
|
||||
* @throws SignatureException if an error occurred while generating a signature
|
||||
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||
* entries or to output JAR signature, or if the engine is closed
|
||||
*/
|
||||
@Deprecated
|
||||
OutputApkSigningBlockRequest outputZipSections(
|
||||
DataSource zipEntries,
|
||||
DataSource zipCentralDirectory,
|
||||
DataSource zipEocd)
|
||||
throws IOException, ApkFormatException, NoSuchAlgorithmException,
|
||||
InvalidKeyException, SignatureException, IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the ZIP sections comprising the output APK have been output.
|
||||
*
|
||||
* <p>The provided data sources are guaranteed to not be used by the engine after this method
|
||||
* terminates.
|
||||
*
|
||||
* @param zipEntries the section of ZIP archive containing Local File Header records and data of
|
||||
* the ZIP entries. In a well-formed archive, this section starts at the start of the
|
||||
* archive and extends all the way to the ZIP Central Directory.
|
||||
* @param zipCentralDirectory ZIP Central Directory section
|
||||
* @param zipEocd ZIP End of Central Directory (EoCD) record
|
||||
*
|
||||
* @return request to add an APK Signing Block to the output or {@code null} if the output must
|
||||
* not contain an APK Signing Block. The request must be fulfilled before
|
||||
* {@link #outputDone()} is invoked.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs while reading the provided ZIP sections
|
||||
* @throws ApkFormatException if the provided APK is malformed in a way which prevents this
|
||||
* engine from producing a valid signature. For example, if the APK Signing Block
|
||||
* provided to the engine is malformed.
|
||||
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||
* cryptographic algorithm implementation is missing
|
||||
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||
* not suitable for generating the signature
|
||||
* @throws SignatureException if an error occurred while generating a signature
|
||||
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||
* entries or to output JAR signature, or if the engine is closed
|
||||
*/
|
||||
OutputApkSigningBlockRequest2 outputZipSections2(
|
||||
DataSource zipEntries,
|
||||
DataSource zipCentralDirectory,
|
||||
DataSource zipEocd)
|
||||
throws IOException, ApkFormatException, NoSuchAlgorithmException,
|
||||
InvalidKeyException, SignatureException, IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the signed APK was output.
|
||||
*
|
||||
* <p>This does not change the output APK. The method helps the client confirm that the current
|
||||
* output is signed.
|
||||
*
|
||||
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||
* entries or to output signatures, or if the engine is closed
|
||||
*/
|
||||
void outputDone() throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Generates a V4 signature proto and write to output file.
|
||||
*
|
||||
* @param data Input data to calculate a verity hash tree and hash root
|
||||
* @param outputFile To store the serialized V4 Signature.
|
||||
* @param ignoreFailures Whether any failures will be silently ignored.
|
||||
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||
* not suitable for generating the signature
|
||||
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||
* cryptographic algorithm implementation is missing
|
||||
* @throws SignatureException if an error occurred while generating a signature
|
||||
* @throws IOException if protobuf fails to be serialized and written to file
|
||||
*/
|
||||
void signV4(DataSource data, File outputFile, boolean ignoreFailures)
|
||||
throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException;
|
||||
|
||||
/**
|
||||
* Checks if the signing configuration provided to the engine is capable of creating a
|
||||
* SourceStamp.
|
||||
*/
|
||||
default boolean isEligibleForSourceStamp() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Generates the digest of the certificate used to sign the source stamp. */
|
||||
default byte[] generateSourceStampCertificateDigest() throws SignatureException {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates to this engine that it will no longer be used. Invoking this on an already closed
|
||||
* engine is OK.
|
||||
*
|
||||
* <p>This does not change the output APK. For example, if the output APK is not yet fully
|
||||
* signed, it will remain so after this method terminates.
|
||||
*/
|
||||
@Override
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Instructions about how to handle an input APK's JAR entry.
|
||||
*
|
||||
* <p>The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and
|
||||
* may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in
|
||||
* which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is
|
||||
* invoked.
|
||||
*/
|
||||
public static class InputJarEntryInstructions {
|
||||
private final OutputPolicy mOutputPolicy;
|
||||
private final InspectJarEntryRequest mInspectJarEntryRequest;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
|
||||
* output policy and without a request to inspect the entry.
|
||||
*/
|
||||
public InputJarEntryInstructions(OutputPolicy outputPolicy) {
|
||||
this(outputPolicy, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
|
||||
* output mode and with the provided request to inspect the entry.
|
||||
*
|
||||
* @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no
|
||||
* need to inspect the entry.
|
||||
*/
|
||||
public InputJarEntryInstructions(
|
||||
OutputPolicy outputPolicy,
|
||||
InspectJarEntryRequest inspectJarEntryRequest) {
|
||||
mOutputPolicy = outputPolicy;
|
||||
mInspectJarEntryRequest = inspectJarEntryRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the output policy for this entry.
|
||||
*/
|
||||
public OutputPolicy getOutputPolicy() {
|
||||
return mOutputPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request to inspect the JAR entry or {@code null} if there is no need to
|
||||
* inspect the entry.
|
||||
*/
|
||||
public InspectJarEntryRequest getInspectJarEntryRequest() {
|
||||
return mInspectJarEntryRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output policy for an input APK's JAR entry.
|
||||
*/
|
||||
public static enum OutputPolicy {
|
||||
/** Entry must not be output. */
|
||||
SKIP,
|
||||
|
||||
/** Entry should be output. */
|
||||
OUTPUT,
|
||||
|
||||
/** Entry will be output by the engine. The client can thus ignore this input entry. */
|
||||
OUTPUT_BY_ENGINE,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to inspect the specified JAR entry.
|
||||
*
|
||||
* <p>The entry's uncompressed data must be provided to the data sink returned by
|
||||
* {@link #getDataSink()}. Once the entry's data has been provided to the sink, {@link #done()}
|
||||
* must be invoked.
|
||||
*/
|
||||
interface InspectJarEntryRequest {
|
||||
|
||||
/**
|
||||
* Returns the data sink into which the entry's uncompressed data should be sent.
|
||||
*/
|
||||
DataSink getDataSink();
|
||||
|
||||
/**
|
||||
* Indicates that entry's data has been provided in full.
|
||||
*/
|
||||
void done();
|
||||
|
||||
/**
|
||||
* Returns the name of the JAR entry.
|
||||
*/
|
||||
String getEntryName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to add JAR signature (aka v1 signature) to the output APK.
|
||||
*
|
||||
* <p>Entries listed in {@link #getAdditionalJarEntries()} must be added to the output APK after
|
||||
* which {@link #done()} must be invoked.
|
||||
*/
|
||||
interface OutputJarSignatureRequest {
|
||||
|
||||
/**
|
||||
* Returns JAR entries that must be added to the output APK.
|
||||
*/
|
||||
List<JarEntry> getAdditionalJarEntries();
|
||||
|
||||
/**
|
||||
* Indicates that the JAR entries contained in this request were added to the output APK.
|
||||
*/
|
||||
void done();
|
||||
|
||||
/**
|
||||
* JAR entry.
|
||||
*/
|
||||
public static class JarEntry {
|
||||
private final String mName;
|
||||
private final byte[] mData;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code JarEntry} with the provided name and data.
|
||||
*
|
||||
* @param data uncompressed data of the entry. Changes to this array will not be
|
||||
* reflected in {@link #getData()}.
|
||||
*/
|
||||
public JarEntry(String name, byte[] data) {
|
||||
mName = name;
|
||||
mData = data.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of this ZIP entry.
|
||||
*/
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the uncompressed data of this JAR entry.
|
||||
*/
|
||||
public byte[] getData() {
|
||||
return mData.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
|
||||
* signature(s) of the APK are contained in this block.
|
||||
*
|
||||
* <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
|
||||
* output APK such that the block is immediately before the ZIP Central Directory, the offset of
|
||||
* ZIP Central Directory in the ZIP End of Central Directory record must be adjusted
|
||||
* accordingly, and then {@link #done()} must be invoked.
|
||||
*
|
||||
* <p>If the output contains an APK Signing Block, that block must be replaced by the block
|
||||
* contained in this request.
|
||||
*
|
||||
* @deprecated This is now superseded by {@link OutputApkSigningBlockRequest2}.
|
||||
*/
|
||||
@Deprecated
|
||||
interface OutputApkSigningBlockRequest {
|
||||
|
||||
/**
|
||||
* Returns the APK Signing Block.
|
||||
*/
|
||||
byte[] getApkSigningBlock();
|
||||
|
||||
/**
|
||||
* Indicates that the APK Signing Block was output as requested.
|
||||
*/
|
||||
void done();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
|
||||
* signature(s) of the APK are contained in this block.
|
||||
*
|
||||
* <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
|
||||
* output APK such that the block is immediately before the ZIP Central Directory. Immediately
|
||||
* before the APK Signing Block must be padding consists of the number of 0x00 bytes returned by
|
||||
* {@link #getPaddingSizeBeforeApkSigningBlock()}. The offset of ZIP Central Directory in the
|
||||
* ZIP End of Central Directory record must be adjusted accordingly, and then {@link #done()}
|
||||
* must be invoked.
|
||||
*
|
||||
* <p>If the output contains an APK Signing Block, that block must be replaced by the block
|
||||
* contained in this request.
|
||||
*/
|
||||
interface OutputApkSigningBlockRequest2 {
|
||||
/**
|
||||
* Returns the APK Signing Block.
|
||||
*/
|
||||
byte[] getApkSigningBlock();
|
||||
|
||||
/**
|
||||
* Indicates that the APK Signing Block was output as requested.
|
||||
*/
|
||||
void done();
|
||||
|
||||
/**
|
||||
* Returns the number of 0x00 bytes the caller must place immediately before APK Signing
|
||||
* Block.
|
||||
*/
|
||||
int getPaddingSizeBeforeApkSigningBlock();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig;
|
||||
|
||||
/**
|
||||
* This class is intended as a lightweight representation of an APK signature verification issue
|
||||
* where the client does not require the additional textual details provided by a subclass.
|
||||
*/
|
||||
public class ApkVerificationIssue {
|
||||
/* The V2 signer(s) could not be read from the V2 signature block */
|
||||
public static final int V2_SIG_MALFORMED_SIGNERS = 1;
|
||||
/* A V2 signature block exists without any V2 signers */
|
||||
public static final int V2_SIG_NO_SIGNERS = 2;
|
||||
/* Failed to parse a signer's block in the V2 signature block */
|
||||
public static final int V2_SIG_MALFORMED_SIGNER = 3;
|
||||
/* Failed to parse the signer's signature record in the V2 signature block */
|
||||
public static final int V2_SIG_MALFORMED_SIGNATURE = 4;
|
||||
/* The V2 signer contained no signatures */
|
||||
public static final int V2_SIG_NO_SIGNATURES = 5;
|
||||
/* The V2 signer's certificate could not be parsed */
|
||||
public static final int V2_SIG_MALFORMED_CERTIFICATE = 6;
|
||||
/* No signing certificates exist for the V2 signer */
|
||||
public static final int V2_SIG_NO_CERTIFICATES = 7;
|
||||
/* Failed to parse the V2 signer's digest record */
|
||||
public static final int V2_SIG_MALFORMED_DIGEST = 8;
|
||||
/* The V3 signer(s) could not be read from the V3 signature block */
|
||||
public static final int V3_SIG_MALFORMED_SIGNERS = 9;
|
||||
/* A V3 signature block exists without any V3 signers */
|
||||
public static final int V3_SIG_NO_SIGNERS = 10;
|
||||
/* Failed to parse a signer's block in the V3 signature block */
|
||||
public static final int V3_SIG_MALFORMED_SIGNER = 11;
|
||||
/* Failed to parse the signer's signature record in the V3 signature block */
|
||||
public static final int V3_SIG_MALFORMED_SIGNATURE = 12;
|
||||
/* The V3 signer contained no signatures */
|
||||
public static final int V3_SIG_NO_SIGNATURES = 13;
|
||||
/* The V3 signer's certificate could not be parsed */
|
||||
public static final int V3_SIG_MALFORMED_CERTIFICATE = 14;
|
||||
/* No signing certificates exist for the V3 signer */
|
||||
public static final int V3_SIG_NO_CERTIFICATES = 15;
|
||||
/* Failed to parse the V3 signer's digest record */
|
||||
public static final int V3_SIG_MALFORMED_DIGEST = 16;
|
||||
/* The source stamp signer contained no signatures */
|
||||
public static final int SOURCE_STAMP_NO_SIGNATURE = 17;
|
||||
/* The source stamp signer's certificate could not be parsed */
|
||||
public static final int SOURCE_STAMP_MALFORMED_CERTIFICATE = 18;
|
||||
/* The source stamp contains a signature produced using an unknown algorithm */
|
||||
public static final int SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM = 19;
|
||||
/* Failed to parse the signer's signature in the source stamp signature block */
|
||||
public static final int SOURCE_STAMP_MALFORMED_SIGNATURE = 20;
|
||||
/* The source stamp's signature block failed verification */
|
||||
public static final int SOURCE_STAMP_DID_NOT_VERIFY = 21;
|
||||
/* An exception was encountered when verifying the source stamp */
|
||||
public static final int SOURCE_STAMP_VERIFY_EXCEPTION = 22;
|
||||
/* The certificate digest in the APK does not match the expected digest */
|
||||
public static final int SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH = 23;
|
||||
/*
|
||||
* The APK contains a source stamp signature block without a corresponding stamp certificate
|
||||
* digest in the APK contents.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST = 24;
|
||||
/*
|
||||
* The APK does not contain the source stamp certificate digest file nor the source stamp
|
||||
* signature block.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING = 25;
|
||||
/*
|
||||
* None of the signatures provided by the source stamp were produced with a known signature
|
||||
* algorithm.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_NO_SUPPORTED_SIGNATURE = 26;
|
||||
/*
|
||||
* The source stamp signer's certificate in the signing block does not match the certificate in
|
||||
* the APK.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK = 27;
|
||||
/* The APK could not be properly parsed due to a ZIP or APK format exception */
|
||||
public static final int MALFORMED_APK = 28;
|
||||
/* An unexpected exception was caught when attempting to verify the APK's signatures */
|
||||
public static final int UNEXPECTED_EXCEPTION = 29;
|
||||
/* The APK contains the certificate digest file but does not contain a stamp signature block */
|
||||
public static final int SOURCE_STAMP_SIG_MISSING = 30;
|
||||
/* Source stamp block contains a malformed attribute. */
|
||||
public static final int SOURCE_STAMP_MALFORMED_ATTRIBUTE = 31;
|
||||
/* Source stamp block contains an unknown attribute. */
|
||||
public static final int SOURCE_STAMP_UNKNOWN_ATTRIBUTE = 32;
|
||||
/**
|
||||
* Failed to parse the SigningCertificateLineage structure in the source stamp
|
||||
* attributes section.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_MALFORMED_LINEAGE = 33;
|
||||
/**
|
||||
* The source stamp certificate does not match the terminal node in the provided
|
||||
* proof-of-rotation structure describing the stamp certificate history.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_POR_CERT_MISMATCH = 34;
|
||||
/**
|
||||
* The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record
|
||||
* with signature(s) that did not verify.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_POR_DID_NOT_VERIFY = 35;
|
||||
/** No V1 / jar signing signature blocks were found in the APK. */
|
||||
public static final int JAR_SIG_NO_SIGNATURES = 36;
|
||||
/** An exception was encountered when parsing the V1 / jar signer in the signature block. */
|
||||
public static final int JAR_SIG_PARSE_EXCEPTION = 37;
|
||||
/** The source stamp timestamp attribute has an invalid value. */
|
||||
public static final int SOURCE_STAMP_INVALID_TIMESTAMP = 38;
|
||||
|
||||
private final int mIssueId;
|
||||
private final String mFormat;
|
||||
private final Object[] mParams;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ApkVerificationIssue} using the provided {@code format} string and
|
||||
* {@code params}.
|
||||
*/
|
||||
public ApkVerificationIssue(String format, Object... params) {
|
||||
mIssueId = -1;
|
||||
mFormat = format;
|
||||
mParams = params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ApkVerificationIssue} using the provided {@code issueId} and {@code
|
||||
* params}.
|
||||
*/
|
||||
public ApkVerificationIssue(int issueId, Object... params) {
|
||||
mIssueId = issueId;
|
||||
mFormat = null;
|
||||
mParams = params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the numeric ID for this issue.
|
||||
*/
|
||||
public int getIssueId() {
|
||||
return mIssueId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the optional parameters for this issue.
|
||||
*/
|
||||
public Object[] getParams() {
|
||||
return mParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// If this instance was created by a subclass with a format string then return the same
|
||||
// formatted String as the subclass.
|
||||
if (mFormat != null) {
|
||||
return String.format(mFormat, mParams);
|
||||
}
|
||||
StringBuilder result = new StringBuilder("mIssueId: ").append(mIssueId);
|
||||
for (Object param : mParams) {
|
||||
result.append(", ").append(param.toString());
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig;
|
||||
|
||||
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
|
||||
import com.android.apksig.internal.apk.v1.V1SchemeConstants;
|
||||
import com.android.apksig.internal.apk.v2.V2SchemeConstants;
|
||||
import com.android.apksig.internal.apk.v3.V3SchemeConstants;
|
||||
|
||||
/**
|
||||
* Exports internally defined constants to allow clients to reference these values without relying
|
||||
* on internal code.
|
||||
*/
|
||||
public class Constants {
|
||||
private Constants() {}
|
||||
|
||||
public static final int VERSION_SOURCE_STAMP = 0;
|
||||
public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
|
||||
public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
|
||||
public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
|
||||
public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31;
|
||||
public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
|
||||
|
||||
/**
|
||||
* The maximum number of signers supported by the v1 and v2 APK Signature Schemes.
|
||||
*/
|
||||
public static final int MAX_APK_SIGNERS = 10;
|
||||
|
||||
/**
|
||||
* The default page alignment for native library files in bytes.
|
||||
*/
|
||||
public static final short LIBRARY_PAGE_ALIGNMENT_BYTES = 16384;
|
||||
|
||||
public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
|
||||
|
||||
public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
|
||||
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
|
||||
|
||||
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
|
||||
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
|
||||
public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID =
|
||||
V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
|
||||
public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
|
||||
|
||||
public static final int V1_SOURCE_STAMP_BLOCK_ID =
|
||||
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
|
||||
public static final int V2_SOURCE_STAMP_BLOCK_ID =
|
||||
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
|
||||
|
||||
public static final String OID_RSA_ENCRYPTION = "1.2.840.113549.1.1.1";
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.apksig;
|
||||
import java.io.IOException;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class Hints {
|
||||
/**
|
||||
* Name of hint pattern asset file in APK.
|
||||
*/
|
||||
public static final String PIN_HINT_ASSET_ZIP_ENTRY_NAME = "assets/com.android.hints.pins.txt";
|
||||
|
||||
/**
|
||||
* Name of hint byte range data file in APK. Keep in sync with PinnerService.java.
|
||||
*/
|
||||
public static final String PIN_BYTE_RANGE_ZIP_ENTRY_NAME = "pinlist.meta";
|
||||
|
||||
private static int clampToInt(long value) {
|
||||
return (int) Math.max(0, Math.min(value, Integer.MAX_VALUE));
|
||||
}
|
||||
|
||||
public static final class ByteRange {
|
||||
final long start;
|
||||
final long end;
|
||||
|
||||
public ByteRange(long start, long end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class PatternWithRange {
|
||||
final Pattern pattern;
|
||||
final long offset;
|
||||
final long size;
|
||||
|
||||
public PatternWithRange(String pattern) {
|
||||
this.pattern = Pattern.compile(pattern);
|
||||
this.offset= 0;
|
||||
this.size = Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
public PatternWithRange(String pattern, long offset, long size) {
|
||||
this.pattern = Pattern.compile(pattern);
|
||||
this.offset = offset;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public Matcher matcher(CharSequence input) {
|
||||
return this.pattern.matcher(input);
|
||||
}
|
||||
|
||||
public ByteRange ClampToAbsoluteByteRange(ByteRange rangeIn) {
|
||||
if (rangeIn.end - rangeIn.start < this.offset) {
|
||||
return null;
|
||||
}
|
||||
long rangeOutStart = rangeIn.start + this.offset;
|
||||
long rangeOutSize = Math.min(rangeIn.end - rangeOutStart,
|
||||
this.size);
|
||||
return new ByteRange(rangeOutStart,
|
||||
rangeOutStart + rangeOutSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a blob of bytes that PinnerService understands as a
|
||||
* sequence of byte ranges to pin.
|
||||
*/
|
||||
public static byte[] encodeByteRangeList(List<ByteRange> pinByteRanges) {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream(pinByteRanges.size() * 8);
|
||||
DataOutputStream out = new DataOutputStream(bos);
|
||||
try {
|
||||
for (ByteRange pinByteRange : pinByteRanges) {
|
||||
out.writeInt(clampToInt(pinByteRange.start));
|
||||
out.writeInt(clampToInt(pinByteRange.end - pinByteRange.start));
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new AssertionError("impossible", ex);
|
||||
}
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
public static ArrayList<PatternWithRange> parsePinPatterns(byte[] patternBlob) {
|
||||
ArrayList<PatternWithRange> pinPatterns = new ArrayList<>();
|
||||
try {
|
||||
for (String rawLine : new String(patternBlob, "UTF-8").split("\n")) {
|
||||
String line = rawLine.replaceFirst("#.*", ""); // # starts a comment
|
||||
String[] fields = line.split(" ");
|
||||
if (fields.length == 1) {
|
||||
pinPatterns.add(new PatternWithRange(fields[0]));
|
||||
} else if (fields.length == 3) {
|
||||
long start = Long.parseLong(fields[1]);
|
||||
long end = Long.parseLong(fields[2]);
|
||||
pinPatterns.add(new PatternWithRange(fields[0], start, end - start));
|
||||
} else {
|
||||
throw new AssertionError("bad pin pattern line " + line);
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedEncodingException ex) {
|
||||
throw new RuntimeException("UTF-8 must be supported", ex);
|
||||
}
|
||||
return pinPatterns;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# apksig ([commit ac5cbb07d87cc342fcf07715857a812305d69888](https://android.googlesource.com/platform/tools/apksig/+/ac5cbb07d87cc342fcf07715857a812305d69888))
|
||||
|
||||
apksig is a project which aims to simplify APK signing and checking whether APK signatures are
|
||||
expected to verify on Android. apksig supports
|
||||
[JAR signing](https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File)
|
||||
(used by Android since day one) and
|
||||
[APK Signature Scheme v2](https://source.android.com/security/apksigning/v2.html) (supported since
|
||||
Android Nougat, API Level 24). apksig is meant to be used outside of Android devices.
|
||||
|
||||
The key feature of apksig is that it knows about differences in APK signature verification logic
|
||||
between different versions of the Android platform. apksig thus thoroughly checks whether an APK's
|
||||
signature is expected to verify on all Android platform versions supported by the APK. When signing
|
||||
an APK, apksig chooses the most appropriate cryptographic algorithms based on the Android platform
|
||||
versions supported by the APK being signed.
|
||||
|
||||
## apksig library
|
||||
|
||||
apksig library offers three primitives:
|
||||
|
||||
* `ApkSigner` which signs the provided APK so that it verifies on all Android platform versions
|
||||
supported by the APK. The range of platform versions can be customized.
|
||||
* `ApkVerifier` which checks whether the provided APK is expected to verify on all Android
|
||||
platform versions supported by the APK. The range of platform versions can be customized.
|
||||
* `(Default)ApkSignerEngine` which abstracts away signing APKs from parsing and building APKs.
|
||||
This is useful in optimized APK building pipelines, such as in Android Plugin for Gradle,
|
||||
which need to perform signing while building an APK, instead of after. For simpler use cases
|
||||
where the APK to be signed is available upfront, the `ApkSigner` above is easier to use.
|
||||
|
||||
_NOTE: Some public classes of the library are in packages having the word "internal" in their name.
|
||||
These are not public API of the library. Do not use \*.internal.\* classes directly because these
|
||||
classes may change any time without regard to existing clients outside of `apksig` and `apksigner`._
|
||||
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,911 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig;
|
||||
|
||||
import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||
import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V3;
|
||||
import static com.android.apksig.Constants.VERSION_JAR_SIGNATURE_SCHEME;
|
||||
import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
|
||||
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
|
||||
import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
|
||||
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.apk.ApkUtilsLite;
|
||||
import com.android.apksig.internal.apk.ApkSigResult;
|
||||
import com.android.apksig.internal.apk.ApkSignerInfo;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureInfo;
|
||||
import com.android.apksig.internal.apk.SignatureNotFoundException;
|
||||
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
|
||||
import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
|
||||
import com.android.apksig.internal.apk.v2.V2SchemeConstants;
|
||||
import com.android.apksig.internal.apk.v3.V3SchemeConstants;
|
||||
import com.android.apksig.internal.util.AndroidSdkVersion;
|
||||
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||
import com.android.apksig.internal.zip.CentralDirectoryRecord;
|
||||
import com.android.apksig.internal.zip.LocalFileRecord;
|
||||
import com.android.apksig.internal.zip.ZipUtils;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.util.DataSources;
|
||||
import com.android.apksig.zip.ZipFormatException;
|
||||
import com.android.apksig.zip.ZipSections;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* APK source stamp verifier intended only to verify the validity of the stamp signature.
|
||||
*
|
||||
* <p>Note, this verifier does not validate the signatures of the jar signing / APK signature blocks
|
||||
* when obtaining the digests for verification. This verifier should only be used in cases where
|
||||
* another mechanism has already been used to verify the APK signatures.
|
||||
*/
|
||||
public class SourceStampVerifier {
|
||||
private final File mApkFile;
|
||||
private final DataSource mApkDataSource;
|
||||
|
||||
private final int mMinSdkVersion;
|
||||
private final int mMaxSdkVersion;
|
||||
|
||||
private SourceStampVerifier(
|
||||
File apkFile,
|
||||
DataSource apkDataSource,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion) {
|
||||
mApkFile = apkFile;
|
||||
mApkDataSource = apkDataSource;
|
||||
mMinSdkVersion = minSdkVersion;
|
||||
mMaxSdkVersion = maxSdkVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the APK's source stamp signature and returns the result of the verification.
|
||||
*
|
||||
* <p>The APK's source stamp can be considered verified if the result's {@link
|
||||
* Result#isVerified()} returns {@code true}. If source stamp verification fails all of the
|
||||
* resulting errors can be obtained from {@link Result#getAllErrors()}, or individual errors
|
||||
* can be obtained as follows:
|
||||
* <ul>
|
||||
* <li>Obtain the generic errors via {@link Result#getErrors()}
|
||||
* <li>Obtain the V2 signers via {@link Result#getV2SchemeSigners()}, then for each signer
|
||||
* query for any errors with {@link Result.SignerInfo#getErrors()}
|
||||
* <li>Obtain the V3 signers via {@link Result#getV3SchemeSigners()}, then for each signer
|
||||
* query for any errors with {@link Result.SignerInfo#getErrors()}
|
||||
* <li>Obtain the source stamp signer via {@link Result#getSourceStampInfo()}, then query
|
||||
* for any stamp errors with {@link Result.SourceStampInfo#getErrors()}
|
||||
* </ul>
|
||||
*/
|
||||
public SourceStampVerifier.Result verifySourceStamp() {
|
||||
return verifySourceStamp(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
|
||||
* the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
|
||||
* of the verification.
|
||||
*
|
||||
* <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
|
||||
* if present, without verifying the actual source stamp certificate used to sign the source
|
||||
* stamp. This can be used to verify an APK contains a properly signed source stamp without
|
||||
* verifying a particular signer.
|
||||
*
|
||||
* @see #verifySourceStamp()
|
||||
*/
|
||||
public SourceStampVerifier.Result verifySourceStamp(String expectedCertDigest) {
|
||||
Closeable in = null;
|
||||
try {
|
||||
DataSource apk;
|
||||
if (mApkDataSource != null) {
|
||||
apk = mApkDataSource;
|
||||
} else if (mApkFile != null) {
|
||||
RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
|
||||
in = f;
|
||||
apk = DataSources.asDataSource(f, 0, f.length());
|
||||
} else {
|
||||
throw new IllegalStateException("APK not provided");
|
||||
}
|
||||
return verifySourceStamp(apk, expectedCertDigest);
|
||||
} catch (IOException e) {
|
||||
Result result = new Result();
|
||||
result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
|
||||
return result;
|
||||
} finally {
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided {@code apk}'s source stamp signature, including verification of the
|
||||
* SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and
|
||||
* returns the result of the verification.
|
||||
*
|
||||
* @see #verifySourceStamp(String)
|
||||
*/
|
||||
private SourceStampVerifier.Result verifySourceStamp(DataSource apk,
|
||||
String expectedCertDigest) {
|
||||
Result result = new Result();
|
||||
try {
|
||||
ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
|
||||
// Attempt to obtain the source stamp's certificate digest from the APK.
|
||||
List<CentralDirectoryRecord> cdRecords =
|
||||
ZipUtils.parseZipCentralDirectory(apk, zipSections);
|
||||
CentralDirectoryRecord sourceStampCdRecord = null;
|
||||
for (CentralDirectoryRecord cdRecord : cdRecords) {
|
||||
if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
|
||||
sourceStampCdRecord = cdRecord;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the source stamp's certificate digest is not available within the APK then the
|
||||
// source stamp cannot be verified; check if a source stamp signing block is in the
|
||||
// APK's signature block to determine the appropriate status to return.
|
||||
if (sourceStampCdRecord == null) {
|
||||
boolean stampSigningBlockFound;
|
||||
try {
|
||||
ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
|
||||
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
|
||||
stampSigningBlockFound = true;
|
||||
} catch (SignatureNotFoundException e) {
|
||||
stampSigningBlockFound = false;
|
||||
}
|
||||
result.addVerificationError(stampSigningBlockFound
|
||||
? ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST
|
||||
: ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Verify that the contents of the source stamp certificate digest match the expected
|
||||
// value, if provided.
|
||||
byte[] sourceStampCertificateDigest =
|
||||
LocalFileRecord.getUncompressedData(
|
||||
apk,
|
||||
sourceStampCdRecord,
|
||||
zipSections.getZipCentralDirectoryOffset());
|
||||
if (expectedCertDigest != null) {
|
||||
String actualCertDigest = ApkSigningBlockUtilsLite.toHex(
|
||||
sourceStampCertificateDigest);
|
||||
if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
|
||||
result.addVerificationError(
|
||||
ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
|
||||
actualCertDigest, expectedCertDigest);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
|
||||
new HashMap<>();
|
||||
if (mMaxSdkVersion >= AndroidSdkVersion.P) {
|
||||
SignatureInfo signatureInfo;
|
||||
try {
|
||||
signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
|
||||
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
|
||||
} catch (SignatureNotFoundException e) {
|
||||
signatureInfo = null;
|
||||
}
|
||||
if (signatureInfo != null) {
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
|
||||
ContentDigestAlgorithm.class);
|
||||
parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V3,
|
||||
apkContentDigests, result);
|
||||
signatureSchemeApkContentDigests.put(
|
||||
VERSION_APK_SIGNATURE_SCHEME_V3, apkContentDigests);
|
||||
}
|
||||
}
|
||||
|
||||
if (mMaxSdkVersion >= AndroidSdkVersion.N && (mMinSdkVersion < AndroidSdkVersion.P ||
|
||||
signatureSchemeApkContentDigests.isEmpty())) {
|
||||
SignatureInfo signatureInfo;
|
||||
try {
|
||||
signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
|
||||
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
|
||||
} catch (SignatureNotFoundException e) {
|
||||
signatureInfo = null;
|
||||
}
|
||||
if (signatureInfo != null) {
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
|
||||
ContentDigestAlgorithm.class);
|
||||
parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V2,
|
||||
apkContentDigests, result);
|
||||
signatureSchemeApkContentDigests.put(
|
||||
VERSION_APK_SIGNATURE_SCHEME_V2, apkContentDigests);
|
||||
}
|
||||
}
|
||||
|
||||
if (mMinSdkVersion < AndroidSdkVersion.N
|
||||
|| signatureSchemeApkContentDigests.isEmpty()) {
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests =
|
||||
getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections, result);
|
||||
signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
|
||||
apkContentDigests);
|
||||
}
|
||||
|
||||
ApkSigResult sourceStampResult =
|
||||
V2SourceStampVerifier.verify(
|
||||
apk,
|
||||
zipSections,
|
||||
sourceStampCertificateDigest,
|
||||
signatureSchemeApkContentDigests,
|
||||
mMinSdkVersion,
|
||||
mMaxSdkVersion);
|
||||
result.mergeFrom(sourceStampResult);
|
||||
return result;
|
||||
} catch (ApkFormatException | IOException | ZipFormatException e) {
|
||||
result.addVerificationError(ApkVerificationIssue.MALFORMED_APK, e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
|
||||
} catch (SignatureNotFoundException e) {
|
||||
result.addVerificationError(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses each signer in the provided APK V2 / V3 signature block and populates corresponding
|
||||
* {@code SignerInfo} of the provided {@code result} and their {@code apkContentDigests}.
|
||||
*
|
||||
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||
* expected to be encountered on an Android platform version in the
|
||||
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||
*/
|
||||
public static void parseSigners(
|
||||
ByteBuffer apkSignatureSchemeBlock,
|
||||
int apkSigSchemeVersion,
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||
Result result) {
|
||||
boolean isV2Block = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||
// Both the V2 and V3 signature blocks contain the following:
|
||||
// * length-prefixed sequence of length-prefixed signers
|
||||
ByteBuffer signers;
|
||||
try {
|
||||
signers = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(apkSignatureSchemeBlock);
|
||||
} catch (ApkFormatException e) {
|
||||
result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS
|
||||
: ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS);
|
||||
return;
|
||||
}
|
||||
if (!signers.hasRemaining()) {
|
||||
result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS
|
||||
: ApkVerificationIssue.V3_SIG_NO_SIGNERS);
|
||||
return;
|
||||
}
|
||||
|
||||
CertificateFactory certFactory;
|
||||
try {
|
||||
certFactory = CertificateFactory.getInstance("X.509");
|
||||
} catch (CertificateException e) {
|
||||
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
|
||||
}
|
||||
while (signers.hasRemaining()) {
|
||||
Result.SignerInfo signerInfo = new Result.SignerInfo();
|
||||
if (isV2Block) {
|
||||
result.addV2Signer(signerInfo);
|
||||
} else {
|
||||
result.addV3Signer(signerInfo);
|
||||
}
|
||||
try {
|
||||
ByteBuffer signer = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signers);
|
||||
parseSigner(
|
||||
signer,
|
||||
apkSigSchemeVersion,
|
||||
certFactory,
|
||||
apkContentDigests,
|
||||
signerInfo);
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
signerInfo.addVerificationWarning(
|
||||
isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER
|
||||
: ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the provided signer block and populates the {@code result}.
|
||||
*
|
||||
* <p>This verifies signatures over {@code signed-data} contained in this block but does not
|
||||
* verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
|
||||
* method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
|
||||
* integrity of the APK.
|
||||
*
|
||||
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||
* expected to be encountered on an Android platform version in the
|
||||
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||
*/
|
||||
private static void parseSigner(
|
||||
ByteBuffer signerBlock,
|
||||
int apkSigSchemeVersion,
|
||||
CertificateFactory certFactory,
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||
Result.SignerInfo signerInfo)
|
||||
throws ApkFormatException {
|
||||
boolean isV2Signer = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||
// Both the V2 and V3 signer blocks contain the following:
|
||||
// * length-prefixed signed data
|
||||
// * length-prefixed sequence of length-prefixed digests:
|
||||
// * uint32: signature algorithm ID
|
||||
// * length-prefixed bytes: digest of contents
|
||||
// * length-prefixed sequence of certificates:
|
||||
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
|
||||
ByteBuffer signedData = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signerBlock);
|
||||
ByteBuffer digests = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
|
||||
ByteBuffer certificates = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
|
||||
|
||||
// Parse the digests block
|
||||
while (digests.hasRemaining()) {
|
||||
try {
|
||||
ByteBuffer digest = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(digests);
|
||||
int sigAlgorithmId = digest.getInt();
|
||||
byte[] digestBytes = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(digest);
|
||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||
if (signatureAlgorithm == null) {
|
||||
continue;
|
||||
}
|
||||
apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), digestBytes);
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
signerInfo.addVerificationWarning(
|
||||
isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST
|
||||
: ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the certificates block
|
||||
if (certificates.hasRemaining()) {
|
||||
byte[] encodedCert = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(certificates);
|
||||
X509Certificate certificate;
|
||||
try {
|
||||
certificate = (X509Certificate) certFactory.generateCertificate(
|
||||
new ByteArrayInputStream(encodedCert));
|
||||
} catch (CertificateException e) {
|
||||
signerInfo.addVerificationWarning(
|
||||
isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE
|
||||
: ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE);
|
||||
return;
|
||||
}
|
||||
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||
// form. Without this, getEncoded may return a different form from what was stored in
|
||||
// the signature. This is because some X509Certificate(Factory) implementations
|
||||
// re-encode certificates.
|
||||
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
|
||||
signerInfo.setSigningCertificate(certificate);
|
||||
}
|
||||
|
||||
if (signerInfo.getSigningCertificate() == null) {
|
||||
signerInfo.addVerificationWarning(
|
||||
isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES
|
||||
: ApkVerificationIssue.V3_SIG_NO_CERTIFICATES);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mapping of the {@link ContentDigestAlgorithm} to the {@code byte[]} digest of the
|
||||
* V1 / jar signing META-INF/MANIFEST.MF; if this file is not found then an empty {@code Map} is
|
||||
* returned.
|
||||
*
|
||||
* <p>If any errors are encountered while parsing the V1 signers the provided {@code result}
|
||||
* will be updated to include a warning, but the source stamp verification can still proceed.
|
||||
*/
|
||||
private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
|
||||
List<CentralDirectoryRecord> cdRecords,
|
||||
DataSource apk,
|
||||
ZipSections zipSections,
|
||||
Result result)
|
||||
throws IOException, ApkFormatException {
|
||||
CentralDirectoryRecord manifestCdRecord = null;
|
||||
List<CentralDirectoryRecord> signatureBlockRecords = new ArrayList<>(1);
|
||||
Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
|
||||
ContentDigestAlgorithm.class);
|
||||
for (CentralDirectoryRecord cdRecord : cdRecords) {
|
||||
String cdRecordName = cdRecord.getName();
|
||||
if (cdRecordName == null) {
|
||||
continue;
|
||||
}
|
||||
if (manifestCdRecord == null && MANIFEST_ENTRY_NAME.equals(cdRecordName)) {
|
||||
manifestCdRecord = cdRecord;
|
||||
continue;
|
||||
}
|
||||
if (cdRecordName.startsWith("META-INF/")
|
||||
&& (cdRecordName.endsWith(".RSA")
|
||||
|| cdRecordName.endsWith(".DSA")
|
||||
|| cdRecordName.endsWith(".EC"))) {
|
||||
signatureBlockRecords.add(cdRecord);
|
||||
}
|
||||
}
|
||||
if (manifestCdRecord == null) {
|
||||
// No JAR signing manifest file found. For SourceStamp verification, returning an empty
|
||||
// digest is enough since this would affect the final digest signed by the stamp, and
|
||||
// thus an empty digest will invalidate that signature.
|
||||
return v1ContentDigest;
|
||||
}
|
||||
if (signatureBlockRecords.isEmpty()) {
|
||||
result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES);
|
||||
} else {
|
||||
for (CentralDirectoryRecord signatureBlockRecord : signatureBlockRecords) {
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||
byte[] signatureBlockBytes = LocalFileRecord.getUncompressedData(apk,
|
||||
signatureBlockRecord, zipSections.getZipCentralDirectoryOffset());
|
||||
for (Certificate certificate : certFactory.generateCertificates(
|
||||
new ByteArrayInputStream(signatureBlockBytes))) {
|
||||
// If multiple certificates are found within the signature block only the
|
||||
// first is used as the signer of this block.
|
||||
if (certificate instanceof X509Certificate) {
|
||||
Result.SignerInfo signerInfo = new Result.SignerInfo();
|
||||
signerInfo.setSigningCertificate((X509Certificate) certificate);
|
||||
result.addV1Signer(signerInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (CertificateException e) {
|
||||
// Log a warning for the parsing exception but still proceed with the stamp
|
||||
// verification.
|
||||
result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
|
||||
signatureBlockRecord.getName(), e);
|
||||
break;
|
||||
} catch (ZipFormatException e) {
|
||||
throw new ApkFormatException("Failed to read APK", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
byte[] manifestBytes =
|
||||
LocalFileRecord.getUncompressedData(
|
||||
apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
|
||||
v1ContentDigest.put(
|
||||
ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
|
||||
return v1ContentDigest;
|
||||
} catch (ZipFormatException e) {
|
||||
throw new ApkFormatException("Failed to read APK", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of verifying the APK's source stamp signature; this signature can only be considered
|
||||
* verified if {@link #isVerified()} returns true.
|
||||
*/
|
||||
public static class Result {
|
||||
private final List<SignerInfo> mV1SchemeSigners = new ArrayList<>();
|
||||
private final List<SignerInfo> mV2SchemeSigners = new ArrayList<>();
|
||||
private final List<SignerInfo> mV3SchemeSigners = new ArrayList<>();
|
||||
private final List<List<SignerInfo>> mAllSchemeSigners = Arrays.asList(mV1SchemeSigners,
|
||||
mV2SchemeSigners, mV3SchemeSigners);
|
||||
private SourceStampInfo mSourceStampInfo;
|
||||
|
||||
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||
|
||||
private boolean mVerified;
|
||||
|
||||
void addVerificationError(int errorId, Object... params) {
|
||||
mErrors.add(new ApkVerificationIssue(errorId, params));
|
||||
}
|
||||
|
||||
void addVerificationWarning(int warningId, Object... params) {
|
||||
mWarnings.add(new ApkVerificationIssue(warningId, params));
|
||||
}
|
||||
|
||||
private void addV1Signer(SignerInfo signerInfo) {
|
||||
mV1SchemeSigners.add(signerInfo);
|
||||
}
|
||||
|
||||
private void addV2Signer(SignerInfo signerInfo) {
|
||||
mV2SchemeSigners.add(signerInfo);
|
||||
}
|
||||
|
||||
private void addV3Signer(SignerInfo signerInfo) {
|
||||
mV3SchemeSigners.add(signerInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the APK's source stamp signature
|
||||
*/
|
||||
public boolean isVerified() {
|
||||
return mVerified;
|
||||
}
|
||||
|
||||
private void mergeFrom(ApkSigResult source) {
|
||||
switch (source.signatureSchemeVersion) {
|
||||
case Constants.VERSION_SOURCE_STAMP:
|
||||
mVerified = source.verified;
|
||||
if (!source.mSigners.isEmpty()) {
|
||||
mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown ApkSigResult Signing Block Scheme Id "
|
||||
+ source.signatureSchemeVersion);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link SignerInfo} objects representing the V1 signers of the
|
||||
* provided APK.
|
||||
*/
|
||||
public List<SignerInfo> getV1SchemeSigners() {
|
||||
return mV1SchemeSigners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link SignerInfo} objects representing the V2 signers of the
|
||||
* provided APK.
|
||||
*/
|
||||
public List<SignerInfo> getV2SchemeSigners() {
|
||||
return mV2SchemeSigners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link SignerInfo} objects representing the V3 signers of the
|
||||
* provided APK.
|
||||
*/
|
||||
public List<SignerInfo> getV3SchemeSigners() {
|
||||
return mV3SchemeSigners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link SourceStampInfo} instance representing the source stamp signer for the
|
||||
* APK, or null if the source stamp signature verification failed before the stamp signature
|
||||
* block could be fully parsed.
|
||||
*/
|
||||
public SourceStampInfo getSourceStampInfo() {
|
||||
return mSourceStampInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if an error was encountered while verifying the APK.
|
||||
*
|
||||
* <p>Any error prevents the APK from being considered verified.
|
||||
*/
|
||||
public boolean containsErrors() {
|
||||
if (!mErrors.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (List<SignerInfo> signers : mAllSchemeSigners) {
|
||||
for (SignerInfo signer : signers) {
|
||||
if (signer.containsErrors()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mSourceStampInfo != null) {
|
||||
if (mSourceStampInfo.containsErrors()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the errors encountered while verifying the APK's source stamp.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getErrors() {
|
||||
return mErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the warnings encountered while verifying the APK's source stamp.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all errors for this result, including any errors from signature scheme signers
|
||||
* and the source stamp.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getAllErrors() {
|
||||
List<ApkVerificationIssue> errors = new ArrayList<>();
|
||||
errors.addAll(mErrors);
|
||||
|
||||
for (List<SignerInfo> signers : mAllSchemeSigners) {
|
||||
for (SignerInfo signer : signers) {
|
||||
errors.addAll(signer.getErrors());
|
||||
}
|
||||
}
|
||||
if (mSourceStampInfo != null) {
|
||||
errors.addAll(mSourceStampInfo.getErrors());
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all warnings for this result, including any warnings from signature scheme
|
||||
* signers and the source stamp.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getAllWarnings() {
|
||||
List<ApkVerificationIssue> warnings = new ArrayList<>();
|
||||
warnings.addAll(mWarnings);
|
||||
|
||||
for (List<SignerInfo> signers : mAllSchemeSigners) {
|
||||
for (SignerInfo signer : signers) {
|
||||
warnings.addAll(signer.getWarnings());
|
||||
}
|
||||
}
|
||||
if (mSourceStampInfo != null) {
|
||||
warnings.addAll(mSourceStampInfo.getWarnings());
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains information about an APK's signer and any errors encountered while parsing the
|
||||
* corresponding signature block.
|
||||
*/
|
||||
public static class SignerInfo {
|
||||
private X509Certificate mSigningCertificate;
|
||||
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||
|
||||
void setSigningCertificate(X509Certificate signingCertificate) {
|
||||
mSigningCertificate = signingCertificate;
|
||||
}
|
||||
|
||||
void addVerificationError(int errorId, Object... params) {
|
||||
mErrors.add(new ApkVerificationIssue(errorId, params));
|
||||
}
|
||||
|
||||
void addVerificationWarning(int warningId, Object... params) {
|
||||
mWarnings.add(new ApkVerificationIssue(warningId, params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current signing certificate used by this signer.
|
||||
*/
|
||||
public X509Certificate getSigningCertificate() {
|
||||
return mSigningCertificate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link List} of {@link ApkVerificationIssue} objects representing errors
|
||||
* encountered during processing of this signer's signature block.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getErrors() {
|
||||
return mErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link List} of {@link ApkVerificationIssue} objects representing warnings
|
||||
* encountered during processing of this signer's signature block.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if any errors were encountered while parsing this signer's
|
||||
* signature block.
|
||||
*/
|
||||
public boolean containsErrors() {
|
||||
return !mErrors.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains information about an APK's source stamp and any errors encountered while
|
||||
* parsing the stamp signature block.
|
||||
*/
|
||||
public static class SourceStampInfo {
|
||||
private final List<X509Certificate> mCertificates;
|
||||
private final List<X509Certificate> mCertificateLineage;
|
||||
|
||||
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>();
|
||||
|
||||
private final long mTimestamp;
|
||||
|
||||
/*
|
||||
* Since this utility is intended just to verify the source stamp, and the source stamp
|
||||
* currently only logs warnings to prevent failing the APK signature verification, treat
|
||||
* all warnings as errors. If the stamp verification is updated to log errors this
|
||||
* should be set to false to ensure only errors trigger a failure verifying the source
|
||||
* stamp.
|
||||
*/
|
||||
private static final boolean mWarningsAsErrors = true;
|
||||
|
||||
private SourceStampInfo(ApkSignerInfo result) {
|
||||
mCertificates = result.certs;
|
||||
mCertificateLineage = result.certificateLineage;
|
||||
mErrors.addAll(result.getErrors());
|
||||
mWarnings.addAll(result.getWarnings());
|
||||
mInfoMessages.addAll(result.getInfoMessages());
|
||||
mTimestamp = result.timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SourceStamp's signing certificate or {@code null} if not available. The
|
||||
* certificate is guaranteed to be available if no errors were encountered during
|
||||
* verification (see {@link #containsErrors()}.
|
||||
*
|
||||
* <p>This certificate contains the SourceStamp's public key.
|
||||
*/
|
||||
public X509Certificate getCertificate() {
|
||||
return mCertificates.isEmpty() ? null : mCertificates.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link X509Certificate} instances representing the source
|
||||
* stamp signer's lineage with the oldest signer at element 0, or an empty {@code List}
|
||||
* if the stamp's signing certificate has not been rotated.
|
||||
*/
|
||||
public List<X509Certificate> getCertificatesInLineage() {
|
||||
return mCertificateLineage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether any errors were encountered during the source stamp verification.
|
||||
*/
|
||||
public boolean containsErrors() {
|
||||
return !mErrors.isEmpty() || (mWarningsAsErrors && !mWarnings.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if any info messages were encountered during verification of
|
||||
* this source stamp.
|
||||
*/
|
||||
public boolean containsInfoMessages() {
|
||||
return !mInfoMessages.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link ApkVerificationIssue} representing errors that were
|
||||
* encountered during source stamp verification.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getErrors() {
|
||||
if (!mWarningsAsErrors) {
|
||||
return mErrors;
|
||||
}
|
||||
List<ApkVerificationIssue> result = new ArrayList<>();
|
||||
result.addAll(mErrors);
|
||||
result.addAll(mWarnings);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link ApkVerificationIssue} representing warnings that
|
||||
* were encountered during source stamp verification.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link ApkVerificationIssue} representing info messages
|
||||
* that were encountered during source stamp verification.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getInfoMessages() {
|
||||
return mInfoMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the epoch timestamp in seconds representing the time this source stamp block
|
||||
* was signed, or 0 if the timestamp is not available.
|
||||
*/
|
||||
public long getTimestampEpochSeconds() {
|
||||
return mTimestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder of {@link SourceStampVerifier} instances.
|
||||
*
|
||||
* <p> The resulting verifier, by default, checks whether the APK's source stamp signature will
|
||||
* verify on all platform versions. The APK's {@code android:minSdkVersion} attribute is not
|
||||
* queried to determine the APK's minimum supported level, so the caller should specify a lower
|
||||
* bound with {@link #setMinCheckedPlatformVersion(int)}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final File mApkFile;
|
||||
private final DataSource mApkDataSource;
|
||||
|
||||
private int mMinSdkVersion = 1;
|
||||
private int mMaxSdkVersion = Integer.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code Builder} for source stamp verification of the provided {@code
|
||||
* apk}.
|
||||
*/
|
||||
public Builder(File apk) {
|
||||
if (apk == null) {
|
||||
throw new NullPointerException("apk == null");
|
||||
}
|
||||
mApkFile = apk;
|
||||
mApkDataSource = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code Builder} for source stamp verification of the provided {@code
|
||||
* apk}.
|
||||
*/
|
||||
public Builder(DataSource apk) {
|
||||
if (apk == null) {
|
||||
throw new NullPointerException("apk == null");
|
||||
}
|
||||
mApkDataSource = apk;
|
||||
mApkFile = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the oldest Android platform version for which the APK's source stamp is verified.
|
||||
*
|
||||
* <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
|
||||
* on all Android platforms starting from the platform version with the provided {@code
|
||||
* minSdkVersion}. The upper end of the platform versions range can be modified via
|
||||
* {@link #setMaxCheckedPlatformVersion(int)}.
|
||||
*
|
||||
* @param minSdkVersion API Level of the oldest platform for which to verify the APK
|
||||
*/
|
||||
public SourceStampVerifier.Builder setMinCheckedPlatformVersion(int minSdkVersion) {
|
||||
mMinSdkVersion = minSdkVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the newest Android platform version for which the APK's source stamp is verified.
|
||||
*
|
||||
* <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
|
||||
* on all platform versions up to and including the proviced {@code maxSdkVersion}. The
|
||||
* lower end of the platform versions range can be modified via {@link
|
||||
* #setMinCheckedPlatformVersion(int)}.
|
||||
*
|
||||
* @param maxSdkVersion API Level of the newest platform for which to verify the APK
|
||||
* @see #setMinCheckedPlatformVersion(int)
|
||||
*/
|
||||
public SourceStampVerifier.Builder setMaxCheckedPlatformVersion(int maxSdkVersion) {
|
||||
mMaxSdkVersion = maxSdkVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link SourceStampVerifier} initialized according to the configuration of this
|
||||
* builder.
|
||||
*/
|
||||
public SourceStampVerifier build() {
|
||||
return new SourceStampVerifier(
|
||||
mApkFile,
|
||||
mApkDataSource,
|
||||
mMinSdkVersion,
|
||||
mMaxSdkVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue