How to correctly receive and store a local directory/path in Android preferences?

Viewed 434

I want to store a local path (either internal or external storage) where the user can store some data as a preference in an Android app. I want the user to select a prefered folder in the preferences. Default folder is context.externalMediaDirs which returns a File object whos absolute path I store as string in the SharedPreferences. Later when the user saves the data the data is correctly stored via FileOutputStream to a newly created file in that folder.

Now I added an intent for OPEN_DOCUMENT_TREE on the preference so the user can select a different folder. But the intent returns an URI in the format of content://com.android.externalstorage.documents/... which I store as string, but when I try to write a file via context.contentResolver.openOutputStream() I get the error that I need MANAGE_DOCUMENTS permission. But this is only for system apps.

I found solutions how to convert an URI to a local path, but it was always noted that this is hacky and should not be done. I tried contentResolver.takePersistableUriPermission(...) but nothing changed.

So what is the correct way to receive a folder from the user, store it in the preferences, and be able to create new files there?

Edit: current approach preference XML (extract):

<Preference
    app:key="target_dir">
    <intent android:action="android.intent.action.OPEN_DOCUMENT_TREE"/>
</Preference>

Calling the intent

val targetDirPreference: Preference? = findPreference("target_dir")
targetDirPreference?.setOnPreferenceClickListener {
    startActivityForResult(it.intent, REQUEST_TARGET_FOLDER)
    true
}

Result handling (changed on blackapps request to not include shared preferences but only create a file)

override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
    if ((requestCode == REQUEST_TARGET_FOLDER ) && resultCode == RESULT_OK && intent != null) {

        //val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
        //sharedPreferences.getString("target_dir", "")?.also {
        //    with(sharedPreferences.edit()) {
        //        putString("target_dir", intent.data.toString())
        //        commit()
        //    }
        val newFile = Uri.parse("${intent.dataString}%2Ftest.png")
        context?.contentResolver?.openOutputStream(newFile)
        }
    }
    else {
        super.onActivityResult(requestCode, resultCode, intent)
    }
}

Trying to create a new file, some time (maybe hours) after the intent

val targetDir = PreferenceManager.getDefaultSharedPreferences(context).getString("target_dir", null)
val targetFile = "${System.currentTimeMillis()}.png"
val uri = Uri.parse("$targetDir%2F$targetFile")
val stream = context.contentResolver.openOutputStream(uri) 

The last line crashes with the permission error.

1 Answers

You cannot create a document Uri by simple string concatenation. Instead:

  • Use DocumentFile.fromTreeUri() to get a DocumentFile representing the tree

  • On that, call createFile() to get a DocumentFile representing the content that you wish to create

  • On that, call getUri() to get the Uri to use with openOutputStream()

You will still need the takePersistableUriPermissions() call for the tree, if you wish to be able to create documents and sub-trees.

Default folder is context.externalMediaDirs which returns a File object whos absolute path I store as string in the SharedPreferences. Later when the user saves the data the data is correctly stored via FileOutputStream to a newly created file in that folder.

If you are using the 0th index out of the array, that might hold up. Other ones represent removable storage, and if the user replaces their removable storage (e.g., swaps micro SD cards), their directory names may change. Only directories on the built-in external storage are stable.

Related