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