Files
SoulStream/handoff/rounds/ins-r36-kt-body.txt
T
KeelrLeek 9f0601027c android: the handoff — the pending rounds, the pipeline, and what is unverified
Everything written this session that has not been through the device: rounds 43
to 48 (each patch adversarially verified, 43's transient-notice expectation
fixed), a partial round 49, the pipeline scripts, the Android Auto design and
the icon comparison mock. Also the two harness changes that were never A/B'd —
adaptive waits, which may only shorten a declared wait and never lengthen it,
and a ship that runs the mock walk and the deploy at the same time. The A/B and
its 255-step baseline are in handoff/tools; main stays at what the phone has
actually walked.
2026-09-14 07:57:41 -05:00

170 lines
8.0 KiB
Plaintext

/** A FILE from the phone: the document picker with the caller's own
* filter. A document that is already on this phone's storage is handed
* over as it lies — nothing is copied, so a 300 MB pick costs nothing
* — and one that has no path of its own is copied into cacheDir/picked
* under its own display name, as far as `maxCopyBytes` allows and no
* further. Zero refuses the copy outright.
*
* The copies are swept at the start of every pick: pickImage's own
* copies live in the cache for ever, and an audio file is not an
* image. So a path handed out by one pick is gone by the next — read
* it in the same breath.
*/
@Command
fun pickFile(invoke: Invoke) {
val args = invoke.parseArgs(PickFileArgs::class.java)
sweepPicked()
val types = (args.mimeTypes ?: "").split(',').map { it.trim() }.filter { it.isNotEmpty() }
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
if (types.size == 1) {
intent.type = types[0]
} else {
intent.type = "*/*"
if (types.isNotEmpty()) intent.putExtra(Intent.EXTRA_MIME_TYPES, types.toTypedArray())
}
startActivityForResult(invoke, intent, "filePicked")
}
@ActivityCallback
fun filePicked(invoke: Invoke, result: ActivityResult) {
when (result.resultCode) {
Activity.RESULT_OK -> {
val uri = result.data?.data
if (uri == null) { invoke.reject("no file came back"); return }
val info = openableInfo(uri)
val name = info.first ?: "picked"
val stated = info.second
val out = JSObject()
out.put("name", name)
/* the document's own path first, and nothing copied: a row
naming a file beside it still resolves against the real
folder, and the size stops mattering */
val here = documentPath(uri)
if (here != null) {
out.put("path", here)
out.put("copied", false)
if (stated != null) out.put("size", stated)
invoke.resolve(out)
return
}
/* the same Invoke the pick was made with — PluginHandle
holds it in the result closure — so the cap is read back
off the caller's own arguments rather than stashed in a
field a recreated activity would have reset to zero */
val cap = invoke.parseArgs(PickFileArgs::class.java).maxCopyBytes.toLong()
if (cap <= 0L) { invoke.reject("that file is not on this phone's storage"); return }
if (stated != null && stated > cap) {
invoke.reject("that file is " + (stated / (1024 * 1024)) + " MB — too large to copy here")
return
}
try {
val copy = copyInto(uri, name, cap)
if (copy == null) { invoke.reject("that file could not be read"); return }
out.put("path", copy.absolutePath)
out.put("copied", true)
out.put("size", copy.length())
invoke.resolve(out)
} catch (e: TooBig) {
invoke.reject(e.message ?: "that file is too large to copy here")
} catch (e: Exception) {
invoke.reject("that file could not be read: " + e.message)
}
}
Activity.RESULT_CANCELED -> invoke.reject("cancelled")
else -> invoke.reject("the picker failed")
}
}
/** DISPLAY_NAME and SIZE off the provider. The name is load-bearing:
* the Rust parser picks the format off the extension and titles the
* draft from the stem, so a copy under an invented name would read
* every playlist as plain text. SIZE is often absent, and absent is
* not zero — the copy counts for itself. */
private fun openableInfo(uri: Uri): Pair<String?, Long?> {
var name: String? = null
var size: Long? = null
try {
activity.contentResolver.query(uri, null, null, null, null)?.use { c ->
if (c.moveToFirst()) {
val ni = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (ni >= 0 && !c.isNull(ni)) name = c.getString(ni)
val si = c.getColumnIndex(OpenableColumns.SIZE)
if (si >= 0 && !c.isNull(si)) size = c.getLong(si)
}
}
} catch (_: Exception) { /* a provider that answers nothing still has a document */ }
return Pair(name, size)
}
/** The document's own path, when this phone has one for it: the
* Downloads provider's "raw:/storage/…", and the external-storage
* provider's "primary:Music/set.m3u" — the DOCUMENT sibling of the
* tree id pathOf resolves. Null when it names no path, and null when
* this app may not read the one it names (all-files access is not
* always granted, and a path Rust cannot open is worse than none). */
private fun documentPath(uri: Uri): String? {
val id = try { DocumentsContract.getDocumentId(uri) } catch (_: Exception) { return null }
val path = if (id.startsWith("raw:")) {
id.substring(4)
} else if (uri.authority == "com.android.externalstorage.documents") {
val i = id.indexOf(':')
if (i < 0) return null
val volume = id.substring(0, i)
val rel = id.substring(i + 1).trimEnd('/')
val root = if (volume == "primary") Environment.getExternalStorageDirectory().absolutePath else "/storage/$volume"
if (rel.isEmpty()) root else "$root/$rel"
} else {
return null
}
return if (java.io.File(path).canRead()) path else null
}
private fun pickedDir(): java.io.File {
val dir = java.io.File(activity.cacheDir, "picked")
if (!dir.isDirectory) dir.mkdirs()
return dir
}
/** The last pick's copy, gone. A playlist is kilobytes and a DJ set is
* not, and nothing here is worth keeping once the caller has read it. */
private fun sweepPicked() {
try { pickedDir().listFiles()?.forEach { it.delete() } } catch (_: Exception) { /* the copy is a courtesy */ }
}
/** The picked document, copied under its own name so the extension
* survives, counted as it goes so a provider that understated its size
* cannot fill the cache; a partial copy is deleted. */
private fun copyInto(uri: Uri, name: String, cap: Long): java.io.File? {
val target = java.io.File(pickedDir(), safeName(name))
val input = activity.contentResolver.openInputStream(uri) ?: return null
var total = 0L
try {
input.use { i ->
target.outputStream().use { o ->
val buf = ByteArray(64 * 1024)
while (true) {
val n = i.read(buf)
if (n < 0) break
total += n.toLong()
if (total > cap) throw TooBig("that file is larger than " + (cap / (1024 * 1024)) + " MB")
o.write(buf, 0, n)
}
}
}
} catch (e: Exception) {
target.delete()
throw e
}
return target
}
/** A name the cache can hold: the document's own, minus anything that
* could walk out of the folder. What the CALLER parses by is the
* display name, which travels beside the path untouched. */
private fun safeName(name: String): String {
val base = name.substringAfterLast('/').substringAfterLast('\\').trim()
val clean = base.replace(Regex("[^A-Za-z0-9 ._()\\[\\]+&,'-]"), "_")
return if (clean.isEmpty() || clean == "." || clean == "..") "picked" else clean.take(120)
}