plugins { id 'com.android.application' } apply plugin: 'kotlin-android' apply plugin: 'org.jetbrains.kotlin.plugin.compose' def localBuildOptions = new Properties() def localBuildOptionsFile = rootProject.layout.projectDirectory.file('local.properties') if (localBuildOptionsFile.asFile.exists()) { localBuildOptions.load(new StringReader(providers.fileContents(localBuildOptionsFile).asText.get())) } def pythonExecutable = project.findProperty('pythonExecutable') ?: localBuildOptions.getProperty('droiddeck.python', 'python3') def vitureSdkDir = project.hasProperty('vitureSdkDir') ? project.property('vitureSdkDir') : localBuildOptions.getProperty('droiddeck.vitureSdk', '') def uvcDir = project.hasProperty('uvcDir') ? project.property('uvcDir') : localBuildOptions.getProperty('droiddeck.uvcDir', '') if (uvcDir) { ['arm64-v8a/libusb.so', 'arm64-v8a/libuvc.a', 'include/libusb.h', 'include/libuvc/libuvc.h', 'licenses/libusb-COPYING', 'licenses/libuvc-LICENSE.txt'].each { required -> if (!new File(uvcDir, required).isFile()) throw new GradleException("Incomplete UVC build: missing ${required}") } } if (vitureSdkDir) { ['android/arm64-v8a/libglasses.so', 'android/arm64-v8a/libcarina_vio.so', 'android/arm64-v8a/libcloud_protocol.so', 'include/viture_glasses_provider.h', 'android/LICENSE', 'android/NOTICE'].each { required -> if (!new File(vitureSdkDir, required).isFile()) { throw new GradleException("Incomplete VITURE Android SDK: missing ${required}") } } } def gitValue = { List arguments -> try { def output = providers.exec { commandLine(['git'] + arguments) workingDir rootProject.projectDir ignoreExitValue = true } output.result.get().exitValue == 0 ? output.standardOutput.asText.get().trim() : null } catch (Exception ignored) { null } } def buildLabel = providers.environmentVariable('DROIDDECK_BUILD_LABEL').getOrNull() if (buildLabel == null || buildLabel.trim().isEmpty()) { def eventName = providers.environmentVariable('GITHUB_EVENT_NAME').getOrNull() def eventPath = providers.environmentVariable('GITHUB_EVENT_PATH').getOrNull() if (eventName == 'pull_request_target' && eventPath) { try { def eventFile = layout.file(providers.provider { new File(eventPath) }) def eventText = providers.fileContents(eventFile).asText.get() def pullRequest = new groovy.json.JsonSlurper().parseText(eventText).pull_request if (pullRequest?.number && pullRequest?.head?.sha) { buildLabel = "pr${pullRequest.number} at ${pullRequest.head.sha.take(7)}" } } catch (Exception ignored) { } } if (buildLabel == null || buildLabel.trim().isEmpty()) { def branch = providers.environmentVariable('GITHUB_REF_NAME').getOrNull() ?: gitValue(['branch', '--show-current']) ?: 'local' def commit = providers.environmentVariable('GITHUB_SHA').getOrNull() ?.take(7) ?: gitValue(['rev-parse', '--short=7', 'HEAD']) ?: 'unknown' buildLabel = "${branch} at ${commit}" } } def buildOrigin = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true' ? 'remote' : 'local' def treeState = providers.environmentVariable('DROIDDECK_BUILD_TREE_STATE').getOrNull() if (!(treeState in ['clean', 'dirty'])) { def status = gitValue(['status', '--porcelain']) treeState = status == null ? 'unknown' : (status.isEmpty() ? 'clean' : 'dirty') } buildLabel = "${buildOrigin} ${treeState} ยท ${buildLabel}" def escapedBuildLabel = buildLabel.replace('\\', '\\\\').replace('"', '\\"') def sourceCommit = providers.environmentVariable('DROIDDECK_SOURCE_COMMIT').getOrNull() ?: providers.environmentVariable('GITHUB_SHA').getOrNull() ?: gitValue(['rev-parse', 'HEAD']) ?: '' def prNumber = (providers.environmentVariable('DROIDDECK_PR_NUMBER').getOrNull() ?: '0').with { it.isInteger() ? it : '0' } def prTitle = providers.environmentVariable('DROIDDECK_PR_TITLE').getOrNull() ?: '' def buildBranch = providers.environmentVariable('GITHUB_HEAD_REF').getOrNull()?.trim() ?: gitValue(['branch', '--show-current']) ?: '' def escapeJava = { String value -> value.replace('\\', '\\\\').replace('"', '\\"').replace('\n', ' ').replace('\r', ' ') } def ciBuild = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true' def releaseSigner = rootProject.file('keystore/release-signer.sha256').text.trim() def commitTime = (gitValue(['show', '-s', '--format=%ct', '--end-of-options', sourceCommit]) ?: '0').with { it.isLong() ? it : '0' } def desktopKde = rootProject.file('tools/desktop-kde/release.env').readLines() .findAll { it.contains('=') && !it.startsWith('#') } .collectEntries { def kv = it.split('=', 2); [(kv[0].trim()): kv[1].trim()] } def trackingPack = rootProject.file('tools/packs/tracking/release.env').readLines() .findAll { it.contains('=') && !it.startsWith('#') } .collectEntries { def kv = it.split('=', 2); [(kv[0].trim()): kv[1].trim()] } def prootPackPin = rootProject.file('tools/packs/proot/release.env').readLines() .findAll { it.contains('=') && !it.startsWith('#') } .collectEntries { def kv = it.split('=', 2); [(kv[0].trim()): kv[1].trim()] } def addonPacks = ['proot', 'directaudio', 'tracking', 'xbox', 'comet', 'butler', 'powervr', 'ubisoft'].collectEntries { name -> def pack = file("build/packs/${name}.tar.zst") if (!pack.isFile()) { if (ciBuild) throw new GradleException("Add-on pack ${pack} is missing: run tools/packs/build.sh app/build/packs") if (name == 'proot' && prootPackPin.PROOT_PACK_SHA256) { return [(name): [sha256: prootPackPin.PROOT_PACK_SHA256, size: (prootPackPin.PROOT_PACK_SIZE ?: '0') as long]] } return [(name): [sha256: '', size: 0L]] } def digest = java.security.MessageDigest.getInstance('SHA-256') pack.withInputStream { input -> byte[] buffer = new byte[1 << 20] int n while ((n = input.read(buffer)) > 0) digest.update(buffer, 0, n) } [(name): [sha256: digest.digest().encodeHex().toString(), size: pack.length()]] } def desktopRemovalSources = new groovy.json.JsonSlurper().parse(file('src/main/assets/desktop-removal/sources.json')) if (desktopRemovalSources.archives[desktopKde.DESKTOP_KDE_TAG] != desktopKde.DESKTOP_KDE_SHA256) { throw new GradleException('Desktop removal inventories do not match the pinned archive') } if (!file('src/main/assets/desktop-removal/versions.txt').readLines().contains(desktopKde.DESKTOP_KDE_TAG)) { throw new GradleException('Regenerate desktop removal inventories for ' + desktopKde.DESKTOP_KDE_TAG) } def variantSuffixes = rootProject.file('tools/release/variants.txt').readLines() .findAll { it.trim() && !it.startsWith('#') } .collect { def f = it.trim().split(/\s+/); "${f[1]}=${f[2] == '-' ? '' : f[2]}" } .join(';') android { namespace 'com.droiddeck.launcher' compileSdk 34 ndkVersion project.findProperty('ndkVersion') ?: '27.3.13750724' defaultConfig { applicationId "com.droiddeck.launcher" minSdkVersion 26 targetSdkVersion 28 versionCode 19 versionName "0.7.1" buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\"" buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\"" buildConfigField "int", "PR_NUMBER", "${prNumber}" buildConfigField "String", "PR_TITLE", "\"${escapeJava(prTitle)}\"" buildConfigField "String", "BUILD_BRANCH", "\"${escapeJava(buildBranch)}\"" buildConfigField "boolean", "CI_BUILD", "${ciBuild}" buildConfigField "String", "RELEASE_SIGNER", "\"${releaseSigner}\"" buildConfigField "long", "COMMIT_TIME", "${commitTime}L * 1000L" buildConfigField "String", "VARIANT_SUFFIXES", "\"${variantSuffixes}\"" buildConfigField "String", "SGDB_API_KEY", "\"${System.getenv("SGDB_API_KEY") ?: project.findProperty("sgdbApiKey") ?: ""}\"" buildConfigField "String", "DESKTOP_KDE_TAG", "\"${desktopKde.DESKTOP_KDE_TAG}\"" buildConfigField "String", "DESKTOP_KDE_URL", "\"https://github.com/${desktopKde.DESKTOP_KDE_REPO}/releases/download/${desktopKde.DESKTOP_KDE_TAG}/desktop-kde.tar.zst\"" buildConfigField "String", "DESKTOP_KDE_SHA256", "\"${desktopKde.DESKTOP_KDE_SHA256}\"" buildConfigField "long", "DESKTOP_KDE_SIZE", "${desktopKde.DESKTOP_KDE_SIZE}L" addonPacks.each { name, pack -> buildConfigField "String", "PACK_${name.toUpperCase()}_SHA256", "\"${pack.sha256}\"" buildConfigField "long", "PACK_${name.toUpperCase()}_SIZE", "${pack.size}L" } ndk { abiFilters 'arm64-v8a' } externalNativeBuild { cmake { arguments '-DANDROID_STL=c++_shared' arguments "-DDROIDDECK_VITURE_SDK_DIR=${vitureSdkDir.toString().replace('\\', '/')}" arguments "-DDROIDDECK_UVC_DIR=${uvcDir.toString().replace('\\', '/')}" } } } externalNativeBuild { cmake { path 'src/main/cpp/CMakeLists.txt' version '3.22.1' } } signingConfigs { testkey { storeFile file("${rootDir}/keystore/testkey.p12") storeType "PKCS12" storePassword "android" keyAlias "testkey" keyPassword "android" enableV1Signing = true enableV2Signing = true enableV3Signing = true } } buildTypes { debug { minifyEnabled false signingConfig signingConfigs.testkey def localSuffix = localBuildOptions.getProperty('droiddeck.debugApplicationIdSuffix') if (localSuffix) { applicationIdSuffix localSuffix resValue 'string', 'app_name', 'DroidDeck XR Dev' } } release { minifyEnabled false signingConfig signingConfigs.testkey } } lint { disable 'ExpiredTargetSdkVersion' } androidResources { ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:_*:!CVS:!thumbs.db:!picasa.ini:!*~:droiddeck-xbox:droiddeck-comet:droiddeck-ubisoft:directaudio:hybris' } packagingOptions { resources { excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF'] } jniLibs { useLegacyPackaging = true pickFirsts += ['**/libtermux.so'] excludes += ['**/libproot.so', '**/libproot-loader.so', '**/libopencv_java4.so', '**/libmediapipe_tasks_jni.so'] } } buildFeatures { prefab true compose true buildConfig true } testOptions { unitTests.returnDefaultValues = true unitTests.includeAndroidResources = true unitTests.all { if (ciBuild) { outputs.upToDateWhen { false } outputs.doNotCacheIf('CI executes the test suite on every run') { true } } maxHeapSize = '2g' maxParallelForks = 2 def logTarget = providers.systemProperty('robolectric.logging').orNull if (logTarget != null) systemProperty 'robolectric.logging', logTarget } } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = '17' } } // No instrumented tests; the unused androidTest variant breaks the VS Code Kotlin import. androidComponents { beforeVariants(selector().all()) { it.androidTest.enable = false } } // Vendor binaries remain in the user's SDK directory; never vendor them into Git. if (vitureSdkDir) { android.sourceSets.main.jniLibs.srcDir(new File(vitureSdkDir, 'android')) def stageVitureNotices = tasks.register('stageVitureNotices', Copy) { from(new File(vitureSdkDir, 'android')) { include 'LICENSE', 'NOTICE' } into(layout.buildDirectory.dir('vitureAssets/licenses/viture')) } tasks.named('preBuild').configure { dependsOn(stageVitureNotices) } } def stageSessionScripts = tasks.register('stageSessionScripts', Copy) { from(rootProject.file('tools/linuxfs/overlay')) { include 'usr/local/bin/droiddeck-*', 'usr/local/bin/steam-compatibility', 'usr/bin/**' } into(layout.projectDirectory.dir('src/main/assets/linuxfs')) } tasks.named('preBuild').configure { dependsOn(stageSessionScripts) } def runtimeAssetsDir = layout.buildDirectory.dir('runtimeAssets') def stageRuntimeAssets = tasks.register('stageRuntimeAssets', Sync) { dependsOn(stageSessionScripts) from(layout.projectDirectory.dir('src/main/assets')) { exclude 'pulseaudio.tzst' } from(rootProject.file('tools/linuxfs/innoextract')) from(layout.buildDirectory.dir('prepared-assets')) into(runtimeAssetsDir) } android.sourceSets.main.assets.setSrcDirs([runtimeAssetsDir]) if (vitureSdkDir) android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('vitureAssets')) if (uvcDir) { def stageUvcNotices = tasks.register('stageUvcNotices', Copy) { from(new File(uvcDir, 'licenses')) into(layout.buildDirectory.dir('uvcAssets/licenses/uvc')) } tasks.named('preBuild').configure { dependsOn(stageUvcNotices) } android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('uvcAssets')) } tasks.named('preBuild').configure { dependsOn(stageRuntimeAssets) } def stageGogNotices = tasks.register('stageGogNotices', Copy) { from(layout.projectDirectory.dir('src/main/rust/stores/vendor/xdelta3')) { include 'LICENSE', 'README.droiddeck.md' } into(layout.buildDirectory.dir('gogNotices/licenses/xdelta3')) } android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('gogNotices')) tasks.named('preBuild').configure { dependsOn(stageGogNotices) } if (!ciBuild) { def stageLocalPacks = tasks.register('stageLocalPacks', Sync) { from(layout.buildDirectory.dir('packs')) { include '*.tar.zst' } into(layout.buildDirectory.dir('localPackAssets/packs')) } android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('localPackAssets')) tasks.named('preBuild').configure { dependsOn(stageLocalPacks) } } def verifyRuntimeInputs = tasks.register('verifyRuntimeInputs', Exec) { mustRunAfter(stageSessionScripts, stageRuntimeAssets) workingDir rootProject.projectDir def allowStale = (project.findProperty('allowStaleRuntime') ?: localBuildOptions.getProperty('droiddeck.allowStaleRuntime', 'false')).toString().toBoolean() commandLine([pythonExecutable, 'tools/release/runtime_inputs.py', 'verify'] + (allowStale ? ['--allow-stale-sources'] : [])) } tasks.configureEach { task -> if (task.name in ['packageDebug', 'packageRelease']) task.dependsOn(verifyRuntimeInputs) } def rustCrateDir = layout.projectDirectory.dir('src/main/rust/stores') def rustJniLibsDir = layout.buildDirectory.dir('rustJniLibs') def skipRust = (project.findProperty('skipRust') ?: localBuildOptions.getProperty('droiddeck.skipRust', 'false')).toString().toBoolean() def buildRustStores = tasks.register('buildRustStores', Exec) { description = 'Cross-compiles the store download engines (libdroiddeckstores.so) with cargo-ndk.' onlyIf { !skipRust } inputs.dir(rustCrateDir.dir('src')) inputs.dir(rustCrateDir.dir('vendor')) inputs.file(rustCrateDir.file('build.rs')) inputs.files(rustCrateDir.file('Cargo.toml'), rustCrateDir.file('Cargo.lock')) inputs.property('ndkVersion', android.ndkVersion) if (!skipRust) inputs.property('rustToolchain', providers.exec { environment 'PATH', "${System.getProperty('user.home')}/.cargo/bin${File.pathSeparator}${System.getenv('PATH')}" commandLine 'rustup', 'run', 'stable', 'rustc', '-Vv' }.standardOutput.asText) outputs.dir(rustJniLibsDir) workingDir rustCrateDir def cargoBin = "${System.getProperty('user.home')}/.cargo/bin" environment 'PATH', "${cargoBin}${File.pathSeparator}${System.getenv('PATH')}" environment 'ANDROID_NDK_HOME', android.ndkDirectory.absolutePath environment 'CARGO_TERM_COLOR', 'never' if (!skipRust) environment 'RUSTC', providers.exec { commandLine 'rustup', 'which', '--toolchain', 'stable', 'rustc' }.standardOutput.asText.get().trim() commandLine 'rustup', 'run', 'stable', 'cargo', 'ndk', '--target', 'arm64-v8a', '--platform', '26', '--output-dir', rustJniLibsDir.get().asFile.absolutePath, 'build', '--release', '--locked' } if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir) tasks.named('preBuild').configure { dependsOn(buildRustStores) } def maliBridgeDir = rootProject.file('external/MaliBridge') def vkbridgeJniLibsDir = layout.buildDirectory.dir('vkbridgeJniLibs') def buildVkBridgeServer = tasks.register('buildVkBridgeServer', Exec) { description = 'Builds the Vulkan bridge server (libvkbridge_server.so) with the NDK.' inputs.dir(new File(maliBridgeDir, 'common')) inputs.dir(new File(maliBridgeDir, 'server')) inputs.dir(new File(maliBridgeDir, 'third_party')) inputs.file(new File(maliBridgeDir, 'scripts/build-server-android.sh')) outputs.dir(vkbridgeJniLibsDir) commandLine 'bash', new File(maliBridgeDir, 'scripts/build-server-android.sh').absolutePath, android.ndkDirectory.absolutePath, vkbridgeJniLibsDir.get().asFile.absolutePath } android.sourceSets.main.jniLibs.srcDir(vkbridgeJniLibsDir) tasks.named('preBuild').configure { dependsOn(buildVkBridgeServer) } dependencies { implementation "org.opencv:opencv:${trackingPack.OPENCV_VERSION}" implementation 'org.khronos.openxr:openxr_loader_for_android:1.1.49' implementation 'com.github.luben:zstd-jni:1.5.7-4@aar' implementation 'org.apache.commons:commons-compress:1.27.1' implementation 'org.tukaani:xz:1.10' implementation platform('androidx.compose:compose-bom:2024.02.00') implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.material3:material3' implementation 'androidx.compose.ui:ui-tooling-preview' debugImplementation 'androidx.compose.ui:ui-tooling' implementation 'androidx.activity:activity-compose:1.8.2' implementation 'androidx.compose.material:material-icons-extended' implementation 'io.coil-kt:coil-compose:2.6.0' implementation 'com.caverock:androidsvg-aar:1.4' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' implementation 'com.flyfishxu:kadb-android:1.2.1' implementation 'com.termux.termux-app:terminal-view:0.118.0' implementation 'androidx.camera:camera-camera2:1.4.2' implementation 'androidx.camera:camera-lifecycle:1.4.2' implementation 'androidx.webkit:webkit:1.12.1' implementation 'io.github.webrtc-sdk:android:125.6422.07' implementation("com.google.mediapipe:tasks-vision:${trackingPack.MEDIAPIPE_VERSION}") { exclude group: 'com.google.auto.value', module: 'auto-value' } implementation 'com.google.zxing:core:3.5.3' testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.16.1' testImplementation 'androidx.compose.ui:ui-test-junit4' testRuntimeOnly 'com.github.luben:zstd-jni:1.5.7-4@jar' }