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.
65 lines
3.4 KiB
Plaintext
65 lines
3.4 KiB
Plaintext
/** A file written wherever the user points it: Android's create-document
|
|
* sheet, and the bytes through the ContentResolver — a content URI is
|
|
* not a path, so `std::fs` on the Rust side could never have written
|
|
* this one. The file arrives whole (the playlist export builds it with
|
|
* the desktop's own writers), so nothing here knows a format. */
|
|
@Command
|
|
fun createDocument(invoke: Invoke) {
|
|
val args = invoke.parseArgs(CreateDocumentArgs::class.java)
|
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
intent.type = args.mime ?: "application/octet-stream"
|
|
intent.putExtra(Intent.EXTRA_TITLE, args.name ?: "playlist")
|
|
startActivityForResult(invoke, intent, "documentCreated")
|
|
}
|
|
|
|
@ActivityCallback
|
|
fun documentCreated(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 }
|
|
try {
|
|
/* The file comes off the SAME invoke, read a second time
|
|
rather than parked in a field of ours: PluginHandle's
|
|
activity-result closure holds the Invoke, so its
|
|
arguments are still here when the sheet comes back, and
|
|
no call of this can hand its bytes to another's file. */
|
|
val text = invoke.parseArgs(CreateDocumentArgs::class.java).text ?: ""
|
|
/* "wt" truncates: without it, writing a short file over a
|
|
longer one leaves the old tail behind the new bytes. */
|
|
val out = activity.contentResolver.openOutputStream(uri, "wt")
|
|
if (out == null) { invoke.reject("the file could not be opened for writing"); return }
|
|
out.use { o -> o.write(text.toByteArray(Charsets.UTF_8)) }
|
|
val res = JSObject()
|
|
res.put("uri", uri.toString())
|
|
res.put("name", displayName(uri))
|
|
invoke.resolve(res)
|
|
} catch (e: Exception) {
|
|
invoke.reject("the file could not be written: " + e.message)
|
|
}
|
|
}
|
|
/* Deliberately NOT a rejection, where pickFolder and pickImage
|
|
both reject "cancelled": a dismissed save is `Ok(None)` on the
|
|
desktop and the export says nothing at all for it. An error
|
|
here would paint a failure over a choice. */
|
|
Activity.RESULT_CANCELED -> invoke.resolve(JSObject())
|
|
else -> invoke.reject("the file could not be created")
|
|
}
|
|
}
|
|
|
|
/** The name the document actually got — the sheet lets it be changed on
|
|
* the way past. The URI's last segment when the provider will not say. */
|
|
private fun displayName(uri: Uri): String? {
|
|
try {
|
|
activity.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { c ->
|
|
val i = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
if (i >= 0 && c.moveToFirst()) {
|
|
val n = c.getString(i)
|
|
if (!n.isNullOrEmpty()) return n
|
|
}
|
|
}
|
|
} catch (_: Exception) { /* the name is a courtesy; the bytes are written */ }
|
|
return uri.lastPathSegment
|
|
}
|