Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ dependencies {
implementation libs.androidx.compose.ui.graphics
implementation libs.androidx.compose.ui.tooling.preview
implementation libs.androidx.compose.material3
implementation libs.androidx.documentfile
testImplementation libs.junit
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espresso.core
Expand Down
11 changes: 6 additions & 5 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
Expand Down Expand Up @@ -33,6 +30,10 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".DirectoryAccessActivity"
android:exported="false">
</activity>
<service
android:name=".BuildEnvironmentService"
android:label="Godot Gradle Build Environment Service"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package org.godotengine.godot_gradle_build_environment

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.util.Log
import androidx.core.content.ContextCompat.checkSelfPermission
import java.io.BufferedReader
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStreamReader
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

class BuildEnvironment(
private val context: Context,
Expand All @@ -32,10 +38,16 @@ class BuildEnvironment(
private const val ROOTFS_FILENAME = "alpine-android-35-jdk17.tar.xz"
private const val ROOTFS_ASSET_PATH = "linux-rootfs/$ROOTFS_FILENAME"

private const val DIR_ACCESS_WAIT_DURATION = 120_000L // in milliseconds

}

private var currentProcess: Process? = null

private val accessLock = ReentrantLock()
private val accessLockCondition = accessLock.newCondition()
@Volatile private var grantedTreeUri: Uri? = null

private fun getDefaultEnv(): List<String> {
return try {
File(rootfs, "env").readLines()
Expand Down Expand Up @@ -153,41 +165,68 @@ class BuildEnvironment(
return exitCode
}

private fun setupProject(projectPath: String, gradleBuildDir: String): File {
val fullPath = File(projectPath, gradleBuildDir)
val hash = Integer.toHexString(fullPath.absolutePath.hashCode())
val workDir = File(projectRoot, hash)

// Clean up assets from a previous export.
if (workDir.exists()) {
val apkAssetsDir = File(workDir, "src/main/assets")
if (apkAssetsDir.exists()) {
apkAssetsDir.deleteRecursively()
private fun setupProject(projectPath: String, gradleBuildDir: String, outputHandler: (Int, String) -> Unit): File {
var projectTreeUri = FileUtils.getProjectTreeUri(context, projectPath)
if (projectTreeUri == null) {
if (checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
outputHandler(OUTPUT_STDERR, "Project path \"$projectPath\" is not accessible. " +
"Click on the build notification and give ${context.getString(R.string.app_launcher_name)} app access to the project directory.")
Utils.showDirectoryAccessNotification(context, projectPath)
} else {
throw SecurityException("POST_NOTIFICATIONS permission not granted. " +
"Please grant POST_NOTIFICATIONS permission for ${context.getString(R.string.app_launcher_name)} app and retry.")
}

val aabAssetsDir = File(workDir, "assetPackInstallTime/src/main/assets")
if (aabAssetsDir.exists()) {
aabAssetsDir.deleteRecursively()
projectTreeUri = waitForDirectoryAccess(DIR_ACCESS_WAIT_DURATION)
?: throw Exception("Directory access not granted in time. Build canceled.")
outputHandler(OUTPUT_INFO, "Access granted for $projectPath. Starting Gradle build...")
FileUtils.saveProjectTreeUri(context, projectPath, projectTreeUri)

// Notify user if limit is reached so they can clear older projects.
val persistedCount = context.contentResolver.persistedUriPermissions.size
val limit = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) 128 else 512
if (persistedCount == limit) {
outputHandler(OUTPUT_INFO, "Warning: Persisted directory access limit reached." +
"This build will continue, but new projects would require clearing older ones in ${context.getString(R.string.app_launcher_name)} app")
}
}

if (!FileUtils.tryCopyDirectory(fullPath, workDir)) {
throw IOException("Failed to copy $fullPath to $workDir")
val workDir = Utils.getProjectCacheDir(context, projectPath, gradleBuildDir)
if (!workDir.exists()) {
workDir.mkdirs()
ProjectInfo.writeToDirectory(context, workDir, projectPath, gradleBuildDir, projectTreeUri)
}

ProjectInfo.writeToDirectory(workDir, projectPath, gradleBuildDir)

outputHandler(OUTPUT_INFO, "> Importing project files...")
FileUtils.importAndroidProject(context, projectTreeUri, gradleBuildDir, workDir)
return workDir
}

fun cleanProject(projectPath: String, gradleBuildDir: String) {
val fullPath = File(projectPath, gradleBuildDir)
val hash = Integer.toHexString(fullPath.absolutePath.hashCode())
val workDir = File(projectRoot, hash)
private fun fixGradleArgs(projectPath: String, rawGradleArgs: List<String>): List<String> {
val normalizedProjectPath = projectPath.trimEnd('/')
return rawGradleArgs.map { arg ->
when {
arg.startsWith("-Pdebug_keystore_file=") -> "-Pdebug_keystore_file=/project/.android/debug.keystore"
arg.startsWith("-Prelease_keystore_file=") -> "-Prelease_keystore_file=/project/.android/release.keystore"
arg.startsWith("-Paddons_directory=") -> "-Paddons_directory=/project/${FileUtils.ADDONS_DIR_NAME}"

arg.startsWith("-Pplugins_local_binaries=") -> {
val prefix = "-Pplugins_local_binaries="
val value = arg.removePrefix(prefix)
val updated = value.replace("$normalizedProjectPath/${FileUtils.ADDONS_DIR_NAME}", "/project/${FileUtils.ADDONS_DIR_NAME}")
prefix + updated
}
else -> arg
}
}
}

fun cleanProject(projectPath: String, gradleBuildDir: String) {
val workDir = Utils.getProjectCacheDir(context, projectPath, gradleBuildDir)
if (workDir.exists()) {
workDir.deleteRecursively()
}
FileUtils.deleteProjectTreeUri(context, projectPath)
}

fun cleanGlobalCache() {
Expand Down Expand Up @@ -348,14 +387,14 @@ class BuildEnvironment(
return AppPaths.getRootfsReadyFile(File(rootfs)).exists()
}

fun executeGradle(gradleArgs: List<String>, projectPath: String, gradleBuildDir: String, outputHandler: (Int, String) -> Unit): Int {
fun executeGradle(rawGradleArgs: List<String>, projectPath: String, gradleBuildDir: String, outputHandler: (Int, String) -> Unit): Int {
if (!isRootfsReady()) {
outputHandler(OUTPUT_STDERR, "Rootfs isn't installed. Install it in the Godot Gradle Build Environment app.")
return 255
}

val workDir = try {
setupProject(projectPath, gradleBuildDir)
setupProject(projectPath, gradleBuildDir, outputHandler)
} catch (e: Exception) {
outputHandler(OUTPUT_STDERR, "Unable to setup project: ${e.message}")
return 255
Expand All @@ -371,12 +410,14 @@ class BuildEnvironment(
outputHandler(type, line)
}

val gradleArgs = fixGradleArgs(projectPath, rawGradleArgs)

var result = executeGradleInternal(gradleArgs, workDir, captureOutputHandler)

val stderr = stderrBuilder.toString()
if (result == 0 && stderr.contains("BUILD FAILED")) {
// Sometimes Gradle builds fail, but it still gives an exit code of 0.
result = 1;
result = 1
}
stderrBuilder.clear()

Expand All @@ -399,7 +440,7 @@ class BuildEnvironment(
result = executeGradleInternal(gradleArgs, workDir, captureOutputHandler)
val stderr = stderrBuilder.toString()
if (result == 0 && stderr.contains("BUILD FAILED")) {
result = 1;
result = 1
}
}

Expand All @@ -417,4 +458,24 @@ class BuildEnvironment(
}
}

}
fun waitForDirectoryAccess(timeoutMs: Long): Uri? {
Comment thread
syntaxerror247 marked this conversation as resolved.
accessLock.withLock {
var remainingTimeInNanos = TimeUnit.MILLISECONDS.toNanos(timeoutMs)
while (grantedTreeUri == null && remainingTimeInNanos > 0) {
try {
remainingTimeInNanos = accessLockCondition.awaitNanos(remainingTimeInNanos)
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
}
}
return grantedTreeUri
}
}

fun onDirectoryAccessGranted(uri: Uri) {
accessLock.withLock {
grantedTreeUri = uri
accessLockCondition.signalAll()
}
}
Comment thread
syntaxerror247 marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import android.os.Message
import android.os.Messenger
import android.os.RemoteException
import android.util.Log
import androidx.core.net.toUri
import java.util.concurrent.LinkedBlockingQueue

class BuildEnvironmentService : Service() {
Expand All @@ -26,6 +27,7 @@ class BuildEnvironmentService : Service() {
const val MSG_CLEAN_GLOBAL_CACHE = 6
const val MSG_INSTALL_ROOTFS = 7
const val MSG_DELETE_ROOTFS = 8
const val MSG_BUILD_DIR_ACCESS_GRANTED = 9
}

private lateinit var mMessenger: Messenger
Expand Down Expand Up @@ -62,6 +64,12 @@ class BuildEnvironmentService : Service() {
MSG_CLEAN_GLOBAL_CACHE -> queueWork(WorkItem(copy, msg.arg1))
MSG_INSTALL_ROOTFS -> queueWork(WorkItem(copy, msg.arg1))
MSG_DELETE_ROOTFS -> queueWork(WorkItem(copy, msg.arg1))
MSG_BUILD_DIR_ACCESS_GRANTED -> {
val uri = msg.data.getString(Utils.EXTRA_TREE_URI)?.toUri()
if (uri != null) {
mBuildEnvironment.onDirectoryAccessGranted(uri)
}
}
}
}
}
Expand All @@ -88,7 +96,7 @@ class BuildEnvironmentService : Service() {
return
}

Log.i(TAG, "Canceling command: ${id}")
Log.i(TAG, "Canceling command: $id")

if (currentItem?.id == id && currentItem?.msg?.what == MSG_EXECUTE_GRADLE) {
mBuildEnvironment.killCurrentProcess()
Expand Down Expand Up @@ -131,7 +139,7 @@ class BuildEnvironmentService : Service() {
var result = 255

if (args != null && projectPath != null && gradleBuildDir != null) {
result = mBuildEnvironment.executeGradle(args, projectPath, gradleBuildDir, { type, line ->
result = mBuildEnvironment.executeGradle(args, projectPath, gradleBuildDir) { type, line ->
val reply = Message.obtain(null, MSG_COMMAND_OUTPUT, id, type)
val replyData = Bundle()
replyData.putString("line", line)
Expand All @@ -142,7 +150,7 @@ class BuildEnvironmentService : Service() {
} catch (e: RemoteException) {
Log.e(TAG, "Error send command output to client: ${e.message}")
}
})
}
}

val reply = Message.obtain(null, MSG_COMMAND_RESULT, id, result)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package org.godotengine.godot_gradle_build_environment

import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.os.IBinder
import android.os.Message
import android.os.Messenger
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts

class DirectoryAccessActivity : ComponentActivity() {

private var serviceMessenger: Messenger? = null
private var isServiceBound = false

private val directoryPickerLauncher =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri: Uri? ->
if (uri != null) {
val msg = Message.obtain(null, BuildEnvironmentService.MSG_BUILD_DIR_ACCESS_GRANTED)
msg.data = Bundle().apply {
putString(Utils.EXTRA_TREE_URI, uri.toString())
}
serviceMessenger?.send(msg)
}
// Finish and remove from recents screen too
finishAndRemoveTask()
}

private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
serviceMessenger = Messenger(binder)
}

override fun onServiceDisconnected(name: ComponentName?) {
serviceMessenger = null
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleIntent(intent)
}

private fun handleIntent(intent: Intent) {
if (intent.action == Utils.ACTION_REQUEST_DIRECTORY_ACCESS) {
bindToBuildService()

val tempProjectPath = intent.getStringExtra(Utils.EXTRA_PROJECT_PATH)
var initialUri: Uri? = null
if (tempProjectPath != null) {
val externalStorageRoot = Environment.getExternalStorageDirectory().absolutePath
if (tempProjectPath.startsWith(externalStorageRoot)) {
val relativePath = tempProjectPath.replaceFirst(externalStorageRoot, "").trim('/')
initialUri = Uri.Builder()
.scheme("content")
.authority("com.android.externalstorage.documents")
.appendPath("document")
.appendPath("primary:$relativePath")
.build()
}
}
directoryPickerLauncher.launch(initialUri)
}
}

private fun bindToBuildService() {
val intent = Intent(this, BuildEnvironmentService::class.java)
bindService(intent, connection, BIND_AUTO_CREATE)
isServiceBound = true
}

override fun onDestroy() {
if (isServiceBound) {
unbindService(connection)
}
super.onDestroy()
}
}
Loading