Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to regain access to a folder of DocumentFile or DocumentFileCompat outside of callbacks? #110

Closed
lrq3000 opened this issue Jan 4, 2023 · 9 comments
Labels
question Further information is requested

Comments

@lrq3000
Copy link

lrq3000 commented Jan 4, 2023

Library version: 1.5.3
OS version: Android 11
Device model: Pixel 2 (emulator)

Describe the bug
I just want to be able to create DocumentFile files in a folder and be able to reopen them to read and write. The goal is to have a folder outside of the app's internal storage folder, so that other apps can access it, especially file syncing apps. The target app is a TODO list app, so that's why. Accessing a subfolder in the Download folder would be plentily sufficient, but even that fails.

I have tried all the instructions in the readme and in MaterialPreferences and read through both StackOverflow answers and the GitHub issues, the only thing that works is either 1) using a MediaStore/MediaStoreCompat file, but then I lose the ability to reopen files in the future after app's data is wiped (see #103), 2) using the root or folder variables that are returned in the callback just after picking a folder using openFolderPicker or requestStorageAccess, but then how can I store these objects and access again the files in the future? I need a way to store in preferences, and the way is to store as a String as MaterialPreferences does (I copied the code from there), but then it's impossible to reopen files even just to read them!

I also tried to access a subfolder in the Download folder, normally it should be possible without any permission, but it doesn't work either, except with MediaStore and MediaStoreCompat.

Here is the code that I tried, all the commented lines are failed attempts, the uncommented code works but has the limitations I noted above:

https://github.com/lrq3000/OneList/blob/master/app/src/main/java/com/lolo/io/onelist/dialogs/StorageDialog.kt#L74

So would it be possible please to provide an example of how to store and regain access to a DocumentFile or a folder of DocumentFiles outside of the onStorageAccessGranted or onFolderSelected callbacks?

@lrq3000
Copy link
Author

lrq3000 commented Jan 4, 2023

And I know it's not the fault of this library. Android's file permission system is insane since Android 11, it doesn't make any sense how hard it is to access storage via scoped storage permissions, most of the time it just plainly fails and there is no documentation even years later to just explain how to do that. It looks like Google wants most apps to be segregated in their own respective storage spaces.

So thank you very much for making this library. I am looking forward to your valuable inputs that will hopefully help me work out a working app.

@anggrayudi
Copy link
Owner

anggrayudi commented Jan 4, 2023

To request the access, you need to call requestStorageAccess(). On Android 11+, you might need to save the granted path to the preferences, because API 30 can't grant the root path, so you'll need remember which path that you have.

storageHelper.onStorageAccessGranted = { _, root ->
    val preferences = PreferenceManager.getDefaultSharedPreferences(this)
    preferences.edit().putString("path", root.getAbsolutePath(this)).apply()
}

To reuse this granted path in the future, call DocumentFileCompat.fromFullPath():

val preferences = PreferenceManager.getDefaultSharedPreferences(this)
val path = preferences.getString("path", null)
val folder = DocumentFileCompat.fromFullPath(this, path!!, requiresWriteAccess = true)
// now you can make file on this folder
val file = folder?.makeFile(this, "notes", "text/plain")

I'm planning to release SimpleStorage v1.5.4 to help you querying all granted paths in your app, so you don't need to remember them in preferences anymore. Here's the sample code will look like:

val grantedPaths: Map<String, Set<String>> = DocumentFileCompat.getAccessibleAbsolutePaths(context)

Here's the output of that new function:

Screenshot 2023-01-05 at 06 01 10

If you can't wait for the release, just use snapshot version 1.5.4-SNAPSHOT in your dependencies.

@anggrayudi anggrayudi added the question Further information is requested label Jan 4, 2023
@lrq3000
Copy link
Author

lrq3000 commented Jan 7, 2023

Thank you so much, it appears to work! I'm going to revamp my app using the approach you suggested, thank you very much!

The new function would be very helpful, that would be great! But my issue was rather that I did not know how to store and retrieve adequately the permission, your approach fixed it!

Just one question out of curiosity: I notice you suggest to use requestStorageAccess(), why not openFolderPicker()? I thought that FolderPicker was made for this purpose (of selecting a folder tree), no? Or is the FolderPicker only to request read-only access, not write access?

@anggrayudi
Copy link
Owner

anggrayudi commented Jan 7, 2023

If you don't use SimpleStorageHelper, then you need to call requestStorageAccess() in FolderPickerCallback#onStorageAccessDenied() callback. But if you're using it, then you can ignore requestStorageAccess(), because SimpleStorageHelper#openFolderPicker() already handles that capability.

From the code you've shared, you're using SimpleStorageHelper, which is recommended for most cases. Then it's ok to ignore requestStorageAccess() function and SimpleStorageHelper#onStorageAccessGranted() callback. You may call SimpleStorageHelper#openFolderPicker() directly. No need to request access anymore:

buttonSelectFolder.setOnClickListener {
    storageHelper.openFolderPicker()
}

storageHelper.onFolderSelected = { requestCode, folder ->
    // tell user the selected path
}

In the future, you can write a file into the selected folder, for example:

val grantedPaths = DocumentFileCompat.getAccessibleAbsolutePaths(this)
val path = grantedPaths.values.firstOrNull()?.firstOrNull() ?: return
val folder = DocumentFileCompat.fromFullPath(this, path, requiresWriteAccess = true)
val file = folder?.makeFile(this, "notes", "text/plain")

@lrq3000
Copy link
Author

lrq3000 commented Jan 8, 2023

Oh so SimpleStorageHelper.openFolderPicker() is similar to requestStorageAccess(), but requestStorageAccess is more general (can request access to both folders and files) if I understand correctly?

The new function to query grantedPaths looks awesome, so that we won't need to keep the URIs, it will be great also to debug whether we have permission (instead of trying to infer from getting a returned null object)!

I would strongly suggest to add both examples you gave above in the README, as I think this usecase of requesting access to an external shared folder to allow for file syncing is a pretty common one, and your answers are making this much more easy than I thought. I tried to make it work for days and could not!

Also maybe it would be nice to clarify that DocumentFileCompat can be used to get write access too, as the readme makes it sound like it's only to read files, not to write them.

Thank you once again, your library is providing an invaluable service to all of us poor Android developers!

(You can close this issue whenever you want)

@lrq3000
Copy link
Author

lrq3000 commented Jan 8, 2023

(I have misunderstood something in my previous message, please check the edited/corrected comment on github)

@anggrayudi
Copy link
Owner

Oh so SimpleStorageHelper.openFolderPicker() is similar to requestStorageAccess(), but requestStorageAccess is more general (can request access to both folders and files) if I understand correctly?

Basically, if you're not using custom dialog styles, then no need to think much about requestStorageAccess(). You'll understand this function's purpose once you dig into SimpleStorage and SimpleStorageHelper classes.

I would strongly suggest to add both examples you gave above in the README

Sure, I'll do that once v1.5.4 is released.

@anggrayudi
Copy link
Owner

v1.5.4 is released.

@lrq3000
Copy link
Author

lrq3000 commented Jan 10, 2023

This is awesome, thank you so much!

Just a heads up for others who may run into a similar issue, DocumentFile.openOutputStream() does not work anymore if the specified file does not exist.

So this won't work unless the file is created first by another mean:

val preferences2 = PreferenceManager.getDefaultSharedPreferences(appContext)
val path2 = preferences2.getString("path", null)
val path3 = "$path2/testfile.txt"
val file = DocumentFileCompat.fromFullPath(appContext, path3!!, requiresWriteAccess = true) // Don't do it like this, if the file does not exist, trying to openOutputStream() on it will fail. Rather, open the folder, then makeFile with a CreateMode to append.
val out1 = file!!.openOutputStream(appContext, append=true)
try {
    out1!!.write("Some output!".toByteArray(Charsets.UTF_8))
} catch (e: Exception) {
    Log.d("MyApp", "Unable to write test file: " + e.stackTraceToString())
} finally {
    out1?.close()
}

Whereas if we replace the val file line by the following, it will always work (it will create the file if necessary, and if it exists, it will append to it):

val folder = DocumentFileCompat.fromFullPath(appContext, path2!!, requiresWriteAccess = true)
val file = folder?.makeFile(appContext, "testfile.txt", "text/plain", mode=CreateMode.REUSE)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants