Skip to content

Open with: hand *writable* documents over with write access (ACTION_EDIT), so editors can save back #61

Closed

Description

@PeterPablo

What happens

Opening an office document from Twig with "Open with another application" gives the editor a read-only URI: the document loads, but it cannot be saved back (the editor offers no save, or saving fails). Reproduced today with a .xlsx on an Android/HONOR phone: another file manager on the same device hands the same file over so that the editor can save it, so the difference is in Twig.

Twig 1.17.0 (F-Droid), Android 12+.

Why

app/src/main/kotlin/com/twig/app/OpenFiles.kt — openWith() (line ~217) and openWithComponent() (line ~249) both build

val intent = Intent(Intent.ACTION_VIEW).apply {
    setDataAndType(StreamProvider.uriFor(context, file), mimeOf(file.name))
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}

No FLAG_GRANT_WRITE_URI_PERMISSION, no ACTION_EDIT. And the URI handed over is a StreamProvider URI, which is deliberately a read-only stream (proxy fd / one-way pipe, see the class comment) — so granting the write flag alone would only move the failure to save time.

Proposal

Two levels, the first one cheap and safe:

1. Hand writable sources over directly (no provider work). When the underlying source is itself writable — a local File or a SAF document picked via ACTION_OPEN_DOCUMENT/tree access — do not wrap it in StreamProvider; pass the original content:// document URI (SAF) or a FileProvider URI (local, with FLAG_GRANT_WRITE_URI_PERMISSION) to the editor, with

val intent = Intent(Intent.ACTION_EDIT).apply {   // ACTION_EDIT for documents, ACTION_VIEW for media
    setDataAndType(target, mimeOf(file.name))
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}

The editor then writes into the original file, and Twig only has to refresh the pane when it comes back (already the case for other flows). DOC_EXT in the same file already separates documents from media, so the choice of action is a one-liner.

2. Writable StreamProvider (later, for SMB/WebDAV/SFTP sources). For sources whose only handle is a byte stream, write support means the provider must accept write access itself: openFile with mode w/rw returning a temp file that is uploaded on close/release, plus DocumentsContract.Document.FLAG_SUPPORTS_WRITE in query() so editors that check capabilities enable their save path. That is a real feature, not a flag — which is why I am not proposing it as the quick fix.

3. In both cases ACTION_EDIT matters for the resolver: it is what makes the system offer editors (and what some office apps require to open in edit mode rather than read-only).

Not a duplicate (as far as I can tell)

I searched the open issues for write, read-only/readonly, "edit in place" and found nothing about the URI permission of the Open-with path. Issues #14/#15/#16/#17 are about other edit surfaces (hex/database/properties/patches).

Happy to test a build on the device that reproduced it, and to report what each source type does (local file, SAF/Nextcloud, SMB).

Update: a resolved default app does not change it (2026-11-09, evening)

Setting Collabora Office as the system default for the spreadsheet type (HONOR MagicOS:
Einstellungen → Apps → Standard-Apps → Weitere Standard-Apps → Excel-Dokument) does make Twig start Collabora
directly — no chooser, as expected — but the document still opens read-only. So the resolver is not the problem:
the intent and the URI grant are.

Concrete use case to reproduce (Nextcloud + Collabora Office)

  1. Nextcloud (self-hosted, 35.x) with an .xlsx in a folder, opened in Twig both ways — through the SAF
    document tree and through Twig's own WebDAV source. Both were tested on 2026-11-09 and both open read-only, so
    the source is not the variable. (A local copy under /sdcard/Download has not been tested yet; it is not needed to
    reproduce.)
  2. In Twig, open that .xlsx → "Öffnen mit anderer Anwendung" / Open with another application → Collabora
    Office
    (Play Store, current version).
  3. Collabora opens the sheet in read-only/view mode: no editing, no save-back.

The control that isolates the cause

On the same device, the same file, to the same Collabora: another file manager (Ghost Commander) handed it over
and Collabora opened it in an editor that could be used
— i.e. not the view-only state Twig produces. Only Twig's path
is read-only, which rules out Collabora, the file and Android's resolver, and leaves the intent/permission Twig builds
(OpenFiles.openWith(), ACTION_VIEW + FLAG_GRANT_READ_URI_PERMISSION, over a read-only StreamProvider URI). The
comparison is now verified end to end (2026-11-09, evening): with Material Files, the same Nextcloud document
opened through the platform's SAF tree — with the folder granted rather than the account root — opened in Collabora,
accepted an edit, and the save arrived in Nextcloud (13 rows, 6 474 B, previous state kept as a version). An editor
therefore can write back through the SAF path on this device with this Nextcloud; Twig's hand-over is the remaining
difference. (Material Files' own WebDAV source does not save either — it left a 1 024 B file — so the SAF path is the one
that works.)

Two layers, worth separating when fixing

  • Action: an editor is opened with ACTION_VIEW, and some editors (Collabora among them) decide view mode from
    that alone. ACTION_EDIT for DOC_EXT types costs nothing and is a hint the editor understands.
  • Permission/capability: even with ACTION_EDIT and a write grant, the receiving app has to be able to open the
    URI for writing. StreamProvider is a read-only stream by design, so for SMB/WebDAV/SFTP sources this needs the
    writable-provider work described above; for a local file or an SAF document it needs no provider work at all — the
    editor can be given the original URI (or a FileProvider URI with write) instead of the streaming one.

A quick way to tell which layer a given file type hits: if the editor shows its own "read-only" banner while the
system dialog offered write permission, it is the action; if the editor offers "Save" and the save then fails, it is
the provider.


#AI-ASSISTED — this report was drafted with AI assistance; the source references and the reproduced behaviour were checked against main and the device.

Activity

  1. dev2ex commented on Nov 9, 2026

    @dev2ex
    Owner

    Thanks for testing this so thoroughly, especially checking that the edit actually made it back to Nextcloud through the SAF path. That comparison helps pin this down.

    I checked the current code and confirmed that Twig only grants read access, and StreamProvider rejects writes even when the source itself is writable.

    I’ll address local files and SAF documents first, so editors can save back to the original file. That includes your Nextcloud case when opened through its SAF document tree. I’ll also handle the edit intent consistently across the regular Open with flow and opening with a specific app, with a viewing fallback for apps that do not support ACTION_EDIT.

    Save-back through Twig’s own WebDAV, SMB and SFTP sources will remain outside this first fix; those need a separate write-back implementation. I’ll post a test build here when the local/SAF changes are ready.

  2. PeterPablo commented on Nov 9, 2026

    @PeterPablo
    Author

    wow, great to see you responding so fast. For me it was first crucial to understand state of this project -- I have only installed it today and like twig a lot from first "touch point".
    I'll thus keep eyes on this issue, will test and report back.

    Honestly: the thoroughness got only enabled from me since two weeks pairing my local IT experiments/tasks with DeepSeek Flash in VSCode. It is affordable and let's me root cause aspects I would otherwise really have no time to dig into. It on its own pulled the sources, based on my factual description on where things behave differently then expected, and precisely located the code snippet. I have no Android development setup here, albeit I might try it myself...

    So thank you again for the prompt feedback signaling willingness to look into this!

    Note: above bug report got posted without me changing a single character. It was AI created (as stated -- this is my requirement, a best practice from my day job and in my opinion fair, so you can judge on if you are willing to look at it at all).

  3. PeterPablo commented on Nov 9, 2026

    @PeterPablo
    Author

    A follow-up from the same phone, because it isolates the cause further: the same Nextcloud document, opened through a different file manager, is editable and the edit lands back in Nextcloud.

    The working combination is Material Files, with the folder granted through Nextcloud's SAF document tree (not the account root — see the caveat below). Measured: open the .xlsx, change one cell in Collabora Office, save, then check from the web UI — 13 rows, 6,474 B, fresh mtime, valid package.

    What differs is exactly the two layers you described, and Material Files shows both halves:

    So a proxying provider can support write-back — the URI it hands out is not the obstacle, a one-way StreamProvider is. If Twig re-opens the source document with the ParcelFileDescriptor mode translated the way that openFile does, our Nextcloud case should need nothing specific to Nextcloud.

    One caveat that is not yours, in case it looks like the same bug while you test: in Material Files, granting the Nextcloud account root lists every folder twice and fails with java.nio.fileSystemException: /tmp: java.lang.InterruptedException — that is nextcloud/android#17051; granting a subfolder works. The read-only symptom and that root-level defect are independent, so a local file or a subfolder grant is enough to test the write path.

    #AI-ASSISTED

  4. dev2ex commented on Nov 9, 2026

    @dev2ex
    Owner

    Thanks for sharing that — AI assistance is absolutely fine with me. What matters is having a clear description and reproducible behavior, and your checks on the phone have been really helpful.

    The Material Files comparison also clarifies that ACTION_VIEW can work with a writable URI and the right grants. I’ve made the local/SAF changes, including text files, and the automated tests pass. The Collabora + Nextcloud save-back still needs testing on a real device, so your offer to try a build is much appreciated.

    And thanks for the heads-up about the Nextcloud account-root issue. We’ll use a subfolder grant when testing. I’ll share a build here once it’s ready.

  5. dev2ex commented on Nov 9, 2026

    @dev2ex
    Owner

    The test build is ready: 1.18.1-beta.2. It includes the local/SAF fix for documents and text files, including opening through existing app-specific desktop shortcuts.

    For your Nextcloud + Collabora case, please grant a subfolder through SAF, open the spreadsheet in Collabora, make a change and save, then check whether it arrived in Nextcloud’s web UI. Trying a local file as well would help distinguish any remaining hand-off issue from provider synchronization.

    Twig’s own WebDAV, SMB and SFTP sources will continue to open read-only in external apps. We’re keeping that boundary to avoid local working copies silently overwriting newer server changes or presenting a local save as a completed upload. The SAF path delegates saving and synchronization to the original document provider.

    Thanks again for offering to test — I’d be glad to hear how this build behaves on your phone.

  6. PeterPablo commented on Nov 9, 2026

    @PeterPablo
    Author

    Device check passed — 798191bd fixes the Nextcloud/SAF case. Prose from #61 said read-only through Twig; this is the same document, same phone, same editor, one version later.

    Setup: Honor 200 (ELP-NX9), Android 16, Twig 1.18.1-beta.2 (versionCode 321) from the preview APK — signed with the same key as the installed release, so it was a normal update. Path taken: Dokumentenbaum (SAF) → Nextcloud → tmp → .xlsx → Collabora Office.

    1. Collabora opened the document editable — no "schreibgeschützt" banner (that banner was the entire bug).
    2. One cell edited, saved.
    3. In Nextcloud: the file changed 9137 B (19:57) → 6232 B (20:11:11), and Nextcloud stored the state it replaced as a version (…v1794250639, 9137 B, 20:11:10) — so the write went to the original document, not to a cache copy.

    The package is valid, not merely non-empty — this was the failure mode of the other editor I tried (MA Office wrote an unfinalised mixed package):

    • zip integrity ok, 10 parts, Sheet1 intact
    • rows 1..18 (was 1..17) — the new row is there, and the new shared string reads test via new twig
    • the only structural difference: xl/theme/theme.xml became xl/theme/theme1.xml, with [Content_Types].xml and xl/_rels/workbook.xml.rels pointing at the new name — no dangling reference, which is also why the file got smaller

    One leftover, not this fix's business: with the account-root grant the SAF listing still shows every folder twice while opening and saving work fine — the nextcloud/android#17051 symptom, unchanged. Subfolder grants were never involved this time.

    Logcat was no help: the buffer had rolled by the time I read it, so there is no intent dump. The file and its version are the stronger evidence anyway — a write that reached the wrong bytes would not produce row 18 and a consistent package.

    Thanks for turning this around the same evening. #61 can be considered answered from my side.

    #AI-ASSISTED

  7. PeterPablo commented on Nov 9, 2026

    @PeterPablo
    Author

    One leftover, not this fix's business: with the account-root grant the SAF listing still shows every folder twice while opening and saving work fine — the nextcloud/android#17051 symptom, unchanged. Subfolder grants were never involved this time.

    This is optically inconvenient. I shall raise a new issue for that and try compiling myself (again). I did in the meantime compile the app on my laptop, so it seems I would be ready to even compile things locally and load them using adb. nice. :-)
    Thank you again for the very quick turnaround! I guess I leave closing this issue to you, in case you want to follow up on something.

  8. PeterPablo commented on Nov 9, 2026

    @PeterPablo
    Author

    One more data point, then I am quiet: the same document from a document-tree mount rooted at tmp instead of the account root — same result, with its own fingerprint.

    before this run after
    file 6232 B, rows 1..18 6556 B, 20:19:52, rows 1..19
    new shared string — test twig from tmp
    package — integrity ok, Sheet1 intact
    kept state — …v1794251471 (6232 B, 20:19:52)

    So both grant levels write on 1.18.1-beta.2. The difference between them is only the doubled folder listing in the account-root case — and that is more than nextcloud/android#17051 reports for the other file manager I compared, which could not open tmp from the root grant at all (java.nio.fileSystemException). I read that as: with this fix the duplication is a display defect in Twig, not a functional one.

    #AI-ASSISTED

  9. PeterPablo commented on Nov 9, 2026

    @PeterPablo
    Author

    Closing the loop on the leftover I mentioned above, so nobody has to file a follow-up issue for it:

    • It is not a Twig defect. The duplicate entries come from the Nextcloud DocumentsProvider: two rows in its filelist table for one path/file_owner, and the file list query (WHERE parent = ?) has no owner filter and no DISTINCT, so both rows are handed out. Twig maps that cursor 1:1 — which is exactly why the doubling showed up here so plainly.
    • Root-caused and proposed upstream as fix(database): one row per file — unique (path, file_owner) index on filelist nextcloud/android#18001: a migration that collapses the existing duplicates and then puts a UNIQUE INDEX on (path, file_owner) in place, so they cannot come back.
    • Verified with a build carrying that fix, installed beside the current store app on the same phone (separate package, separate authorities): the same tree, added as an external source, lists each entry once. The store app's tree still shows the duplicates, side by side on one screen.
    • Nothing to change on this side. And once that lands, the "grant the subfolder instead of the account root" workaround stops being necessary too.

    Thanks again — the writable-document handover works flawlessly here, and it is the reason the edit-and-save path is usable at all from a file manager.

    #AI-ASSISTED

  10. dev2ex commented on Nov 10, 2026

    @dev2ex
    Owner

    Thanks for the three rounds of device checks — the account-root grant, the tmp-rooted grant, and the provider-level analysis of the doubled folders. That is exactly the evidence that made the remaining question answerable, and the byte-level evidence (row 18/19 present, a valid package, Nextcloud keeping the replaced state as a version) is stronger than any logcat dump.

    Status: fixed on the device — 798191bd in 1.18.1-beta.2. Local files and SAF documents now hand the original handle to the editor (ACTION_EDIT + write grant), so the editor writes into the document its provider owns, and Nextcloud's versioning sees the change.

    The doubled folder listing: understood, and agreed — it is not a Twig defect. The duplicate rows come from the provider's filelist table (two rows for one path/file_owner, and the WHERE parent = ? query has neither an owner filter nor DISTINCT), and Twig maps the cursor 1:1, which is precisely why the doubling was visible here. Your nextcloud/android#18001 — the cleanup migration plus a UNIQUE INDEX on (path, file_owner) — fixes the root cause, and we will keep the 1:1 mapping rather than papering over it with a de-duplication that would hide genuine entries from other providers.

    One boundary stays as announced: Twig's own WebDAV, SMB and SFTP sources still open read-only in external apps. A writable proxy means write-back, temporary-file semantics and an upload on close, and we would rather not present a local save as a completed upload. If that becomes worth doing, it belongs in its own issue.

    Closing this one as fixed — thank you for the report, the control comparisons and the root-causing of the leftover.

    中文翻译:

    感谢你做了三轮设备验证——账户根授权、tmp 为根的授权,以及对重复文件夹的 provider 层分析。正是这些证据让剩下的问题得以有定论,而字节级证据(第 18/19 行确实存在、包结构有效、Nextcloud 把被替换的状态存为历史版本)比任何 logcat 转储都更有说服力。

    状态: 已在设备上修复——798191bd,包含在 1.18.1-beta.2 中。本地文件与 SAF 文档现在把原始句柄交给编辑器(ACTION_EDIT + 写授权),于是编辑器直接写入其 provider 所拥有的文档,Nextcloud 的版本机制也能看到这次改动。

    重复的文件夹列表: 已理解,也认同——这不是 Twig 的缺陷。重复行来自 provider 的 filelist 表(同一个 path/file_owner 有两行,而 WHERE parent = ? 查询既没有 owner 过滤也没有 DISTINCT),而 Twig 是 1:1 映射游标,这正是重复在这里格外显眼的原因。你的 nextcloud/android#18001——清理迁移加上 (path, file_owner) 上的 UNIQUE INDEX——修掉了根因,我们会保留 1:1 映射,而不是用一个会把其他 provider 的真实条目也一起藏起来的去重来掩盖它。

    一条边界仍按先前说明保留: Twig 自带的 WebDAV、SMB 和 SFTP 源在外部应用中仍以只读方式打开。可写代理意味着写回、临时文件语义以及关闭时上传,我们宁愿不把一次本地保存说成已完成的上传。如果这块值得做,应该单开一个 issue。

    这个 issue 就按"已修复"关闭——感谢你的报告、对照实验,以及对遗留问题的根因分析。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions