Skip to content

feat: support folder selection from other apps via Storage Access Framework - #202

Open
alvaroemtnez wants to merge 5 commits into
opencloud-eu:mainfrom
alvaroemtnez:improvement/saf_folder_selection
Open

alvaroemtnez wants to merge 5 commits into
opencloud-eu:mainfrom
alvaroemtnez:improvement/saf_folder_selection

Conversation

@alvaroemtnez

Copy link
Copy Markdown

Summary

OpenCloud has a correct implementation to select its locations from the system files app via the Storage Access Framework. However, as stated in #99 and #45, selecting a folder from an app was not supported, as the ACTION_OPEN_DOCUMENT_TREE intent of the Android Storage Access Framework (SAF) needed to be implemented.

This PR adds support for that intent, and also, as some apps filter the SAF locations and allow only local storage (which OpenCloud can be, if a folder is selected as "available offline", so filtering it makes no sense but hardcoding it as local storage doesn't either), adds a toggle in the Advanced section of settings to pretend to be so, by adding the FLAG_LOCAL_ONLY flag, as other apps like RSAF do, and the corresponding translations for the settings item.

As granting an app access to a location in OpenCloud can mean that the app in question does multiple concurrent operations, light changes have been made, replacing fileToUpload singleton with ConcurrentHashMap and UUIDs to support multi-threaded file creation.

Code changes

  • Added the isChildDocument function override to opencloudApp/src/main/java/eu/opencloud/android/presentation/documentsprovider/DocumentsStorageProvider.kt to implement the ACTION_OPEN_DOCUMENT_TREE intent.
  • Also in opencloudApp/src/main/java/eu/opencloud/android/presentation/documentsprovider/DocumentsStorageProvider.kt, minor changes to use ConcurrentHashMap and random UUIDs for a more robust upload queue.
  • Added support in the addRoot function in opencloudApp/src/main/java/eu/opencloud/android/presentation/documentsprovider/cursors/RootCursor.kt to pass a boolean to enable pretending to be local storage.
  • Added a settings option in the Advanced menu to pretend to be local storage (modified opencloudApp/src/main/res/xml/settings_advanced.xml and opencloudApp/src/main/java/eu/opencloud/android/presentation/settings/advanced/SettingsAdvancedFragment.kt)
  • Added translations for the advanced menu settings item for all supported languages. A subtitle warns the user of the possible unpredictable behaviour pretending to be local storage could originate in slow network connections.

Limitations

  • The URI displayed to the user (if the app in question displays an URI) can be counter-intuitive, as it will be similar to content://eu.opencloud.documents/tree/[ID], where [ID] is an integer defining the directory or file in the internal app tree. As far as I know, displaying the full URI that the user sees in their cloud web UI would require deep and breaking changes (which would probably not be worth, at the moment).
  • Concurrent operations are supported with the UUID based queue (which didn't require deep changes), however, if an app requires thousands of concurrent file operations (i.e., Signal backups, that concurrently read, write, delete, move, etc. files, which is very intensive), that can lead to unexpected behaviour (upload errors, or the entire files directory that signal uses replaced by an empty version). Signal was the only use case I tested where this happened, and only in the very recent new backup system, which is designed to be local only. These use cases seem out of scope for this PR, and would require a regular folder sync like in the desktop app, or a straight WebDAV mount (which RSAF and others already do), which wouldn't be appropriate to implement only to support one edge case. SeedVault could also maybe create the same problems, but again, extremely operation intensive cases seem out of scope when there are solutions specifically tailored for this, and those backups were meant to be done locally, not in a network mount.

AI use

LLMs (Gemini) were used for assistance in code generation and for the translations. All generated code was reviewed and tested and can be easily audited.

@guruz

guruz commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

@zerox80 ^

@zerox80

zerox80 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

ill take a look

@alvaroemtnez

Copy link
Copy Markdown
Author

Thanks for taking the time to check it!

I'll have a look between next week and the other and try to solve everything :)

…k, include a toggle to pretend to be local storage and solve COLUMN_FLAGS being declared twice in RootCursor.kt
@alvaroemtnez
alvaroemtnez force-pushed the improvement/saf_folder_selection branch from 2cdda93 to f65b9c6 Compare September 16, 2026 09:13
@alvaroemtnez

Copy link
Copy Markdown
Author

Sorry it took this long to get to review it.

I just pushed the proposed changes:

  • isChildDocument() now checks the space, together with the account and path.
  • The temporary paths in createFile now have the UUID appended to avoid collisions in concurrent uploads of files with identical names.
  • And about the document IDs and upload queue logic:
    • I dropped the ConcurrentHashMap logic for pending uploads. Now there's a permanent mapping using SharedPreferences (called saf_pending_uploads).
    • There are two new functions in DocumentsStorageProvider.kt for this: savePendingDocument() to save the properties and getPendingDocument() to retreive them. Those functions are then used in openDocument() and queryDocument() where the pendingUploads ConcurrentHashMap was used before.
    • getPendingDocument() also links the UUID to the final database file ID once the upload finishes (by querying its path the first time and caching the dbId). This ensures the pending_ URI remains permanently valid and resolves correctly even if the file is renamed later.
    • As now the mapping is persistent, the line in openDocument where the pending map entry was deleted was removed.
  • Metadata issue: addFile(), in FileCursor.kt now has a parameter to pass the document ID to it. queryDocument(), when handling pending_ files, now explicitly passes the documentId to addFile(file, documentId). This solves the issue where FileCursor was reporting NULL, that was pointed in the third suggestion.

Hope I didn't miss anything else, I don't know the codebase that well yet and I run my OpenCloud instance only for myself and my family (and so, I didn't think of those cases and setups with multiple spaces per user, concurrent uploads with duplicated names, etc).

PS: the force pushes were because I accidentally merged your latest commits instead of rebasing :( fixed though, the PR has 5 commits: 2 from before, and one with every suggested change.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants