    /** 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
    }
