Add 'engine/' from commit 'a8e37fc010'

git-subtree-dir: engine
git-subtree-mainline: b74841629e
git-subtree-split: a8e37fc010
This commit is contained in:
Sara Gerretsen 2026-03-13 11:22:19 +01:00
commit c3f9669b10
14113 changed files with 7458101 additions and 0 deletions

View 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`

View file

@ -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
}
}

View 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"))
}

View 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
}

View 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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>

View 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'

View file

@ -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))
}
}
}
}

View file

@ -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>

View file

@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf

View file

@ -0,0 +1,3 @@
# Godot 4+ specific ignores
/android/
/.godot/editor

View file

@ -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"
}]

View file

@ -0,0 +1,2 @@
source_md5="4cdc64b13a9af63279c486903c9b54cc"
dest_md5="ddbdfc47e6405ad8d8e9e6a88a32824e"

View file

@ -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

View file

@ -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

View file

@ -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))

View file

@ -0,0 +1 @@
uid://bv6y7in6otgcm

View file

@ -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"]

View file

@ -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

View file

@ -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)

View file

@ -0,0 +1 @@
uid://mofa8j0d801f

View file

@ -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

View file

@ -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

View file

@ -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")
}
}
}

View file

@ -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()
}
}
}

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="godot_project_name_string">Godot App Instrumented Tests</string>
</resources>

View file

@ -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>

View file

@ -0,0 +1,2 @@
*
!.gitignore

View file

@ -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;
}
}

View 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")
}

View 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()
}
}
}
}
}

View file

@ -0,0 +1 @@
!/debug

View file

@ -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>

View file

@ -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
}
}

View file

@ -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))
}
}
}
}

View file

@ -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

View file

@ -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
}
}

View 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>

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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";
}

View file

@ -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;
}
}

View file

@ -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`._

View file

@ -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