Here’s how I work around that problem: ask Gemini
Please add a custom Gradle post-build task (copyDebugApk) to app/build.gradle.kts (or the main app-level build.gradle.kts) that automatically copies the generated app-debug.apk to the project root directory whenever assembleDebug runs.
Ensure the task handles race conditions and file locking during Android packaging by waiting for file stabilization and verifying file handles before copying:
Add the necessary imports to the top of build.gradle.kts:
import java.io.File
import java.nio.file.Files
Add the custom CopyApkTask class:
abstract class CopyApkTask : DefaultTask() {
@get:Internal
abstract val apkDir: DirectoryProperty
@get:Internal
abstract val outputDir: DirectoryProperty
private fun hasOtherProcessOpenFile(file: File): Boolean {
val targetCanonicalPath = try {
file.canonicalPath
} catch (e: Exception) {
file.absolutePath
}
val procDir = File("/proc")
if (!procDir.exists() || !procDir.isDirectory) return false
val currentPid = try {
ProcessHandle.current().pid()
} catch (e: Throwable) {
-1L
}
val pids = procDir.listFiles { f -> f.isDirectory && f.name.all { it.isDigit() } } ?: return false
for (pidDir in pids) {
val pidStr = pidDir.name
val pid = pidStr.toLongOrNull() ?: continue
if (pid == currentPid) continue
val fdDir = File(pidDir, "fd")
val fds = fdDir.listFiles() ?: continue
for (fd in fds) {
try {
val linkTarget = Files.readSymbolicLink(fd.toPath()).toFile().canonicalPath
if (linkTarget == targetCanonicalPath) {
println("Process $pid has an open handle to $targetCanonicalPath")
return true
}
} catch (e: Exception) {
// Ignore deleted files, permission errors, or dynamic /proc race conditions
}
}
}
return false
}
@TaskAction
fun run() {
val dir = apkDir.get().asFile
val file = File(dir, "app-debug.apk")
val destDir = outputDir.get().asFile
val destFile = File(destDir, file.name)
// Delay start: Pauses briefly to let file packaging settle
Thread.sleep(1500)
// Wait for source file size to stabilize and check /proc for open handles
var lastSize = -1L
var stableCount = 0
for (attempt in 1..100) {
if (file.exists()) {
val currentSize = file.length()
if (currentSize > 0 && currentSize == lastSize) {
stableCount++
if (stableCount >= 7) {
val sourceHasHandles = hasOtherProcessOpenFile(file)
val destHasHandles = destFile.exists() && hasOtherProcessOpenFile(destFile)
if (sourceHasHandles || destHasHandles) {
println("APK size stable at $currentSize, but open file handles found via /proc. Resetting stableCount (attempt $attempt/100)...")
stableCount = 0
} else {
println("APK is fully stabilized at $currentSize bytes with no open file handles.")
break
}
}
} else {
stableCount = 0
}
lastSize = currentSize
}
Thread.sleep(500)
}
if (file.exists() && file.length() > 0) {
file.copyTo(destFile, overwrite = true)
println("Successfully copied APK to ${destFile.absolutePath} (${destFile.length()} bytes)")
} else {
println("Source APK file not found or empty: ${file.absolutePath}")
}
}
}
Register the task and attach it as finalizedBy on assembleDebug:
val copyDebugApk = tasks.register(“copyDebugApk”) {
apkDir.set(layout.buildDirectory.dir(“outputs/apk/debug”))
outputDir.set(layout.projectDirectory.dir(“..”))
}
afterEvaluate {
tasks.findByName(“assembleDebug”)?.finalizedBy(copyDebugApk)
}
After this, there will be a copy of the debug APK in the project root dir that you can download through the AI studio file explorer. You could probably ask it to do the same for release builds and APKs.
Another trick I have used is to ask Gemini to serve up a file using a web server. This doesn’t seem to persist well but has occasionally been handy. It has taken me multiple prompts to get this working but they could presumably be combined into one:
Can you configure nginx to serve the AAB file as a downloadable link?
That usually works, but on one recent occasion Gemini told me it couldn’t do that, and I had to insist:
Yes you can. You did it before, to serve up the feature graphic that you constructed.
After that, Gemini gave me links to download the AAB file, but on first trying to use it I got a 403 error. If that happens to you, ask Gemini:
That didn’t work. Please disable lua Auth.
BTW, getting Gemini to build debug or release builds of APKs or AABs is just a matter of asking it to.