Skip to content

Commit c45410a

Browse files
Migrate to SAF for file access
1 parent 02577a9 commit c45410a

File tree

12 files changed

+342
-88
lines changed

12 files changed

+342
-88
lines changed

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ dependencies {
8484
implementation libs.androidx.compose.ui.graphics
8585
implementation libs.androidx.compose.ui.tooling.preview
8686
implementation libs.androidx.compose.material3
87+
implementation libs.androidx.documentfile
8788
testImplementation libs.junit
8889
androidTestImplementation libs.androidx.junit
8990
androidTestImplementation libs.androidx.espresso.core

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3-
xmlns:tools="http://schemas.android.com/tools">
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
43

5-
<uses-permission
6-
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
7-
tools:ignore="ScopedStorage" />
4+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
85
<uses-permission
96
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
107
android:maxSdkVersion="29" />

app/src/main/java/org/godotengine/godot_gradle_build_environment/BuildEnvironment.kt

Lines changed: 79 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package org.godotengine.godot_gradle_build_environment
22

3+
import android.Manifest
34
import android.content.Context
5+
import android.content.pm.PackageManager
6+
import android.net.Uri
47
import android.os.Build
5-
import android.os.Environment
68
import android.util.Log
9+
import androidx.core.content.ContextCompat.checkSelfPermission
10+
import androidx.core.net.toUri
711
import java.io.BufferedReader
812
import java.io.File
913
import java.io.FileOutputStream
@@ -36,6 +40,9 @@ class BuildEnvironment(
3640

3741
private var currentProcess: Process? = null
3842

43+
private val accessLock = Object()
44+
@Volatile private var grantedTreeUri: Uri? = null
45+
3946
private fun getDefaultEnv(): List<String> {
4047
return try {
4148
File(rootfs, "env").readLines()
@@ -153,38 +160,58 @@ class BuildEnvironment(
153160
return exitCode
154161
}
155162

156-
private fun setupProject(projectPath: String, gradleBuildDir: String): File {
157-
val fullPath = File(projectPath, gradleBuildDir)
158-
val hash = Integer.toHexString(fullPath.absolutePath.hashCode())
159-
val workDir = File(projectRoot, hash)
160-
161-
// Clean up assets from a previous export.
163+
private fun setupProject(projectPath: String, gradleBuildDir: String, outputHandler: (Int, String) -> Unit): File {
164+
val projectTreeUri: Uri
165+
val workDir = Utils.getProjectCacheDir(context, projectPath, gradleBuildDir)
162166
if (workDir.exists()) {
163-
val apkAssetsDir = File(workDir, "src/main/assets")
164-
if (apkAssetsDir.exists()) {
165-
apkAssetsDir.deleteRecursively()
167+
val info = ProjectInfo.readFromDirectory(workDir)
168+
if (info != null) {
169+
projectTreeUri = info.projectTreeUri.toUri()
170+
} else {
171+
throw Exception("Could not get projectTreeUri.")
166172
}
167-
168-
val aabAssetsDir = File(workDir, "assetPackInstallTime/src/main/assets")
169-
if (aabAssetsDir.exists()) {
170-
aabAssetsDir.deleteRecursively()
173+
} else {
174+
if (checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
175+
outputHandler(OUTPUT_STDERR, "Project path \"$projectPath\" is not accessible. Click on notification and give GABE app access to this directory.")
176+
Utils.showDirectoryAccessNotification(context, projectPath)
177+
} else {
178+
throw SecurityException("POST_NOTIFICATIONS permission not granted. Please grant POST_NOTIFICATIONS permission for GABE app and retry.")
171179
}
172-
}
173180

174-
if (!FileUtils.tryCopyDirectory(fullPath, workDir)) {
175-
throw IOException("Failed to copy $fullPath to $workDir")
181+
// 2 minute wait should be ideal?
182+
projectTreeUri = waitForDirectoryAccess(2 * 60 * 1000)
183+
?: throw Exception("Directory access not granted in time. Build canceled.")
184+
outputHandler(OUTPUT_STDOUT, "Access granted for $projectPath. Starting Gradle build...")
185+
workDir.mkdirs()
186+
ProjectInfo.writeToDirectory(context, workDir, projectPath, gradleBuildDir, projectTreeUri)
176187
}
177188

178-
ProjectInfo.writeToDirectory(workDir, projectPath, gradleBuildDir)
179-
189+
outputHandler(OUTPUT_INFO, "> Importing project files...")
190+
FileUtils.importAndroidProject(context, projectTreeUri, gradleBuildDir, workDir)
180191
return workDir
181192
}
182193

183-
fun cleanProject(projectPath: String, gradleBuildDir: String) {
184-
val fullPath = File(projectPath, gradleBuildDir)
185-
val hash = Integer.toHexString(fullPath.absolutePath.hashCode())
186-
val workDir = File(projectRoot, hash)
194+
private fun fixGradleArgs(projectPath: String, rawGradleArgs: List<String>): List<String> {
195+
val normalizedProjectPath = projectPath.trimEnd('/')
196+
return rawGradleArgs.map { arg ->
197+
when {
198+
arg.startsWith("-Pdebug_keystore_file=") -> "-Pdebug_keystore_file=/project/.android/debug.keystore"
199+
arg.startsWith("-Prelease_keystore_file=") -> "-Prelease_keystore_file=/project/.android/release.keystore"
200+
arg.startsWith("-Paddons_directory=") -> "-Paddons_directory=/project/addons"
201+
202+
arg.startsWith("-Pplugins_local_binaries=") -> {
203+
val prefix = "-Pplugins_local_binaries="
204+
val value = arg.removePrefix(prefix)
205+
val updated = value.replace("$normalizedProjectPath/addons", "/project/addons")
206+
prefix + updated
207+
}
208+
else -> arg
209+
}
210+
}
211+
}
187212

213+
fun cleanProject(projectPath: String, gradleBuildDir: String) {
214+
val workDir = Utils.getProjectCacheDir(context, projectPath, gradleBuildDir)
188215
if (workDir.exists()) {
189216
workDir.deleteRecursively()
190217
}
@@ -336,7 +363,7 @@ class BuildEnvironment(
336363
gradleCmd,
337364
)
338365
val binds = listOf(
339-
Environment.getExternalStorageDirectory().absolutePath,
366+
"/storage/emulated/0:/storage/emulated/0",
340367
"${workDir.absolutePath}:/project",
341368
"${gradleCache.absolutePath}:/project/?",
342369
)
@@ -348,14 +375,14 @@ class BuildEnvironment(
348375
return AppPaths.getRootfsReadyFile(File(rootfs)).exists()
349376
}
350377

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

357384
val workDir = try {
358-
setupProject(projectPath, gradleBuildDir)
385+
setupProject(projectPath, gradleBuildDir, outputHandler)
359386
} catch (e: Exception) {
360387
outputHandler(OUTPUT_STDERR, "Unable to setup project: ${e.message}")
361388
return 255
@@ -371,12 +398,14 @@ class BuildEnvironment(
371398
outputHandler(type, line)
372399
}
373400

401+
val gradleArgs = fixGradleArgs(projectPath, rawGradleArgs)
402+
374403
var result = executeGradleInternal(gradleArgs, workDir, captureOutputHandler)
375404

376405
val stderr = stderrBuilder.toString()
377406
if (result == 0 && stderr.contains("BUILD FAILED")) {
378407
// Sometimes Gradle builds fail, but it still gives an exit code of 0.
379-
result = 1;
408+
result = 1
380409
}
381410
stderrBuilder.clear()
382411

@@ -399,7 +428,7 @@ class BuildEnvironment(
399428
result = executeGradleInternal(gradleArgs, workDir, captureOutputHandler)
400429
val stderr = stderrBuilder.toString()
401430
if (result == 0 && stderr.contains("BUILD FAILED")) {
402-
result = 1;
431+
result = 1
403432
}
404433
}
405434

@@ -417,4 +446,25 @@ class BuildEnvironment(
417446
}
418447
}
419448

420-
}
449+
fun waitForDirectoryAccess(timeoutMs: Long): Uri? {
450+
val endTime = System.currentTimeMillis() + timeoutMs
451+
452+
synchronized(accessLock) {
453+
while (grantedTreeUri == null) {
454+
val remaining = endTime - System.currentTimeMillis()
455+
if (remaining <= 0) {
456+
return null
457+
}
458+
accessLock.wait(remaining)
459+
}
460+
return grantedTreeUri
461+
}
462+
}
463+
464+
fun onDirectoryAccessGranted(uri: Uri) {
465+
synchronized(accessLock) {
466+
grantedTreeUri = uri
467+
accessLock.notifyAll()
468+
}
469+
}
470+
}

app/src/main/java/org/godotengine/godot_gradle_build_environment/BuildEnvironmentService.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import android.os.Message
1111
import android.os.Messenger
1212
import android.os.RemoteException
1313
import android.util.Log
14+
import androidx.core.net.toUri
1415
import java.util.concurrent.LinkedBlockingQueue
1516

1617
class BuildEnvironmentService : Service() {
@@ -26,6 +27,8 @@ class BuildEnvironmentService : Service() {
2627
const val MSG_CLEAN_GLOBAL_CACHE = 6
2728
const val MSG_INSTALL_ROOTFS = 7
2829
const val MSG_DELETE_ROOTFS = 8
30+
31+
const val MSG_RESUME_PENDING_BUILD = 9
2932
}
3033

3134
private lateinit var mMessenger: Messenger
@@ -62,6 +65,12 @@ class BuildEnvironmentService : Service() {
6265
MSG_CLEAN_GLOBAL_CACHE -> queueWork(WorkItem(copy, msg.arg1))
6366
MSG_INSTALL_ROOTFS -> queueWork(WorkItem(copy, msg.arg1))
6467
MSG_DELETE_ROOTFS -> queueWork(WorkItem(copy, msg.arg1))
68+
MSG_RESUME_PENDING_BUILD -> {
69+
val uri = msg.data.getString("tree_uri")?.toUri()
70+
if (uri != null) {
71+
mBuildEnvironment.onDirectoryAccessGranted(uri)
72+
}
73+
}
6574
}
6675
}
6776
}
@@ -88,7 +97,7 @@ class BuildEnvironmentService : Service() {
8897
return
8998
}
9099

91-
Log.i(TAG, "Canceling command: ${id}")
100+
Log.i(TAG, "Canceling command: $id")
92101

93102
if (currentItem?.id == id && currentItem?.msg?.what == MSG_EXECUTE_GRADLE) {
94103
mBuildEnvironment.killCurrentProcess()
@@ -131,7 +140,7 @@ class BuildEnvironmentService : Service() {
131140
var result = 255
132141

133142
if (args != null && projectPath != null && gradleBuildDir != null) {
134-
result = mBuildEnvironment.executeGradle(args, projectPath, gradleBuildDir, { type, line ->
143+
result = mBuildEnvironment.executeGradle(args, projectPath, gradleBuildDir) { type, line ->
135144
val reply = Message.obtain(null, MSG_COMMAND_OUTPUT, id, type)
136145
val replyData = Bundle()
137146
replyData.putString("line", line)
@@ -142,7 +151,7 @@ class BuildEnvironmentService : Service() {
142151
} catch (e: RemoteException) {
143152
Log.e(TAG, "Error send command output to client: ${e.message}")
144153
}
145-
})
154+
}
146155
}
147156

148157
val reply = Message.obtain(null, MSG_COMMAND_RESULT, id, result)

app/src/main/java/org/godotengine/godot_gradle_build_environment/FileUtils.kt

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package org.godotengine.godot_gradle_build_environment
22

3+
import android.content.Context
4+
import android.net.Uri
35
import android.os.Build
46
import android.util.Log
7+
import androidx.documentfile.provider.DocumentFile
58
import java.io.File
6-
import java.io.FileInputStream
79
import java.io.FileOutputStream
810
import java.io.IOException
911
import java.nio.file.Files
@@ -25,21 +27,6 @@ object FileUtils {
2527
return true
2628
}
2729

28-
fun tryCopyDirectory(sourceDir: File, destDir: File): Boolean {
29-
if (!sourceDir.isDirectory) {
30-
Log.e(TAG, "Source directory ${sourceDir.absolutePath} not found")
31-
return false
32-
}
33-
34-
try {
35-
sourceDir.copyRecursively(destDir)
36-
} catch (e: Exception) {
37-
Log.e(TAG, "Failed to copy ${sourceDir.absolutePath} -> ${destDir.absolutePath}: ${e.message}", e)
38-
return false
39-
}
40-
return true
41-
}
42-
4330
/**
4431
* Recursively calculates the total size of a directory in bytes.
4532
*/
@@ -85,4 +72,69 @@ object FileUtils {
8572
return String.format("%.1f %s", value, units[unitIndex])
8673
}
8774

75+
fun importAndroidProject(context: Context, projectTreeUri: Uri, gradleBuildDir: String, destDir: File) {
76+
val root = DocumentFile.fromTreeUri(context, projectTreeUri)
77+
?: throw IOException("Invalid tree uri")
78+
79+
val androidDir = findDirByPath(root, gradleBuildDir)
80+
?: throw IOException("Gradle build dir not found: $gradleBuildDir")
81+
82+
val addonsDir = root.listFiles().firstOrNull {
83+
it.isDirectory && it.name == "addons"
84+
}
85+
86+
if (destDir.exists()) {
87+
val apkAssetsDir = File(destDir, "src/main/assets")
88+
if (apkAssetsDir.exists()) apkAssetsDir.deleteRecursively()
89+
90+
val aabAssetsDir = File(destDir, "assetPackInstallTime/src/main/assets")
91+
if (aabAssetsDir.exists()) aabAssetsDir.deleteRecursively()
92+
} else {
93+
destDir.mkdir()
94+
}
95+
96+
copyDirectoryMerge(context, androidDir, destDir)
97+
98+
if (addonsDir != null) {
99+
val localAddons = File(destDir, "addons")
100+
if (localAddons.exists()) {
101+
localAddons.deleteRecursively()
102+
}
103+
localAddons.mkdirs()
104+
copyDirectoryMerge(context, addonsDir, localAddons)
105+
}
106+
}
107+
108+
private fun findDirByPath(parent: DocumentFile, relativePath: String): DocumentFile? {
109+
var current: DocumentFile? = parent
110+
111+
val parts = relativePath.trim('/').split('/')
112+
113+
for (part in parts) {
114+
current = current?.listFiles()?.firstOrNull {
115+
it.isDirectory && it.name == part
116+
} ?: return null
117+
}
118+
119+
return current
120+
}
121+
122+
private fun copyDirectoryMerge(context: Context, src: DocumentFile, dest: File) {
123+
src.listFiles().forEach { file ->
124+
val name = file.name ?: return@forEach
125+
126+
if (file.isDirectory) {
127+
val newDir = File(dest, name)
128+
if (!newDir.exists()) newDir.mkdirs()
129+
copyDirectoryMerge(context, file, newDir)
130+
} else {
131+
val outFile = File(dest, name)
132+
context.contentResolver.openInputStream(file.uri).use { input ->
133+
FileOutputStream(outFile, false).use { output ->
134+
input?.copyTo(output)
135+
}
136+
}
137+
}
138+
}
139+
}
88140
}

0 commit comments

Comments
 (0)