Related
According to the docs file path access is granted in Android R:
Starting in Android 11, apps that have the READ_EXTERNAL_STORAGE permission can read a device's media files using direct file paths and native libraries. This new capability allows your app to work more smoothly with third-party media libraries.
The problem is that I can't get the file path from MediaStore, so how are we supposed to read a file path that we can't access/retrieve? Is there a way, I'm not aware of, that we can get the file path from MediaStore?
Furthermore, the docs say the following:
All Files Access
Some apps have a core use case that requires broad file access, such as file management or backup & restore operations. They can get All Files Access by doing the following:
Declare the MANAGE_EXTERNAL_STORAGE permission.
Direct users to a system settings page where they can enable the Allow access to manage all files option for your app.
This permission grants the following:
Read access and write access to all files within shared storage.
Access to the contents of the MediaStore.Files table.
But I do not need all file access, I only want the user to select a video from MediaStore and pass the file path to FFmpeg(it requires a file path). I know that I can no longer use the _data column to retrieve a file path.
Please note:
I know a Uri is returned from MediaStore and does not point to a file.
I know that I can copy the file to my application directory and pass that to FFmpeg, but I could do that before Android R.
I can not pass FileDescriptor to FFmpeg and I can not use /proc/self/fd/ (I get /proc/7828/fd/70: Permission denied when selecting a file from the SD Card), have a look at this issue.
So what am I supposed to do, am I missing something? What was meant with can read a device's media files using direct file paths and native libraries?
After asking a question on issuetracker, I've come to the following conclusions:
On Android R, the File restrictions that were added in Android Q is removed. So we can once again access File objects.
If you are targeting Android 10 > and you want to access/use file paths, you will have to add/keep the following in your manifest:
android:requestLegacyExternalStorage="true"
This is to ensure that file paths are working on Android 10(Q). On Android R this attribute will be ignored.
Don't use DATA column for inserting or updating into Media Store, use DISPLAY_NAME and RELATIVE_PATH, here is an example:
ContentValues valuesvideos;
valuesvideos = new ContentValues();
valuesvideos.put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/" + "YourFolder");
valuesvideos.put(MediaStore.Video.Media.TITLE, "SomeName");
valuesvideos.put(MediaStore.Video.Media.DISPLAY_NAME, "SomeName");
valuesvideos.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
valuesvideos.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
valuesvideos.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
valuesvideos.put(MediaStore.Video.Media.IS_PENDING, 1);
ContentResolver resolver = getContentResolver();
Uri collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
Uri uriSavedVideo = resolver.insert(collection, valuesvideos);
You can no longer use the ACTION_OPEN_DOCUMENT_TREE or the ACTION_OPEN_DOCUMENT intent action to request that the user select individual files from Android/data/,Android/obb/and all sub-directories.
It is recommended to only use File objects when you need to perform "seeking", like when using FFmpeg, for example.
You can only use the data column to access files that are on the disk. You should handle I/O Exceptions accordingly.
If you want to access a File or want a file path from a Uri that was returned from MediaStore, I've created a library that handles all the exceptions you might get. This includes all files on the disk, internal and removable disk. When selecting a File from Dropbox, for example, the File will be copied to your applications directory where you have full access, the copied file path will then be returned.
If you are targeting to Android 11 API, you cannot directly get access to the file paths, as there are many restrictions in API 30(Android R). As scoped storage API was introduced in Android 10(API 29), the storage is now divided into scoped storage (private storage) and shared storage (public storage). Scoped storage is a kind you can only have access to the files that are created in your scoped storage directory(i.e. /Android/data/ or /Android/media/<your-package-name>). You cannot access files from shared storage (i.e. internal storage/external SD card storage etc.)
The shared storage is again further divided into Media and Download collection. Media collection stores Image, Audio and Video files. Download collection would take care of non-media files.
To learn in more details about scoped storage and shared storage refer this link: Scoped Storage in Android 10 & Android 11 .
If you are dealing with Media files (i.e. Images, Videos, Audio) you can get the file path by Using Media Store API that having support to API 30(Android 11). and If you are dealing with non-media files(i.e. documents and other files) you can get the file path by using file Uri.
Note: If you are using the file or Uri util classes (such as RealPathUtil, FilePathUtils etc.) to get the file path, here you can get the desired file path but you cannot read that file, as it will throw an exception of Read Access (as Permission denied) in Android 11, as you cannot read the files that are created by another application.
So to achieve this scenario of getting the file path in Android 11(API 30), It a recommended to copy the file into the cache directory of your application using File Uri and get the path of the file access from cache directory.
Here in my scenario I have used both APIs to get the file access in Android 11. To get the file path of the media files (i.e. Images, Videos, Audio), I've used the Media Store API (Refer this link: Media Store API Example - Access media files from shared storage ), and to get the file path of the non-media files (i.e. Documents and other files), I've used fileDescriptor.
File Descriptor Example:
I have created the system dialog file picker to pick the file.
private fun openDocumentAction() {
val mimetypes = arrayOf(
"application/*", //"audio/*",
"font/*", //"image/*",
"message/*",
"model/*",
"multipart/*",
"text/*"
)
// you can customize the mime types as per your choice.
// Choose a directory using the system's file picker.
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
//type = "application/pdf" //only pdf files
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, mimetypes)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
// Optionally, specify a URI for the directory that should be opened in
// the system file picker when it loads.
//putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
startActivityForResult(intent, RC_SAF_NON_MEDIA)
}
And handled the result of file picker in onActivityResult method of the activity. Get the file URI at here.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
RC_SAF_NON_MEDIA -> {
//document selection by SAF(Storage Access Framework) for Android 11
if (resultCode == RESULT_OK) {
// The result data contains a URI for the document or directory that
// the user selected.
data?.data?.also { uri ->
//Permission needed if you want to retain access even after reboot
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
// Perform operations on the document using its URI.
val path = makeFileCopyInCacheDir(uri)
Log.e(localClassName, "onActivityResult: path ${path.toString()} ")
}
}
}
}
}
Pass the file URI to the below method to get the file path. This method will create a file object at cache directory of your application and from that location you can easily get Read access to that file.
private fun makeFileCopyInCacheDir(contentUri :Uri) : String? {
try {
val filePathColumn = arrayOf(
//Base File
MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.TITLE,
MediaStore.Files.FileColumns.DATA,
MediaStore.Files.FileColumns.SIZE,
MediaStore.Files.FileColumns.DATE_ADDED,
MediaStore.Files.FileColumns.DISPLAY_NAME,
//Normal File
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.MediaColumns.DISPLAY_NAME
)
//val contentUri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(mediaUrl))
val returnCursor = contentUri.let { contentResolver.query(it, filePathColumn, null, null, null) }
if (returnCursor!=null) {
returnCursor.moveToFirst()
val nameIndex = returnCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
val name = returnCursor.getString(nameIndex)
val file = File(cacheDir, name)
val inputStream = contentResolver.openInputStream(contentUri)
val outputStream = FileOutputStream(file)
var read = 0
val maxBufferSize = 1 * 1024 * 1024
val bytesAvailable = inputStream!!.available()
//int bufferSize = 1024;
val bufferSize = Math.min(bytesAvailable, maxBufferSize)
val buffers = ByteArray(bufferSize)
while (inputStream.read(buffers).also { read = it } != -1) {
outputStream.write(buffers, 0, read)
}
inputStream.close()
outputStream.close()
Log.e("File Path", "Path " + file.path)
Log.e("File Size", "Size " + file.length())
return file.absolutePath
}
} catch (ex: Exception) {
Log.e("Exception", ex.message!!)
}
return contentUri.let { UriPathUtils().getRealPathFromURI(this, it).toString() }
}
Note: You can use this method to get file path for both media files (Images, Videos, Audio) and non-media files (Documents and other files) as well. Just need to pass a file Uri.
For getting path, i'm coping file with fileDescriptor to new path & i use that path.
Finding File Name:
private static String copyFileAndGetPath(Context context, Uri realUri, String id) {
final String selection = "_id=?";
final String[] selectionArgs = new String[]{id};
String path = null;
Cursor cursor = null;
try {
final String[] projection = {"_display_name"};
cursor = context.getContentResolver().query(realUri, projection, selection, selectionArgs,
null);
cursor.moveToFirst();
final String fileName = cursor.getString(cursor.getColumnIndexOrThrow("_display_name"));
File file = new File(context.getCacheDir(), fileName);
FileUtils.saveAnswerFileFromUri(realUri, file, context);
path = file.getAbsolutePath();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null)
cursor.close();
}
return path;
}
Copy With File Descriptor:
fun saveAnswerFileFromUri(uri: Uri, destFile: File?, context: Context) {
try {
val pfd: ParcelFileDescriptor =
context.contentResolver.openFileDescriptor(uri, "r")!!
if (pfd != null) {
val fd: FileDescriptor = pfd.getFileDescriptor()
val fileInputStream: InputStream = FileInputStream(fd)
val fileOutputStream: OutputStream = FileOutputStream(destFile)
val buffer = ByteArray(1024)
var length: Int
while (fileInputStream.read(buffer).also { length = it } > 0) {
fileOutputStream.write(buffer, 0, length)
}
fileOutputStream.flush()
fileInputStream.close()
fileOutputStream.close()
pfd.close()
}
} catch (e: IOException) {
Timber.w(e)
}
}
I am working on an application where app writes a log file with the currentdate as filename
Ex: 20200710.txt
The earlier was working fine before android 10 but from android 10 the code is no longer writing the file in the external storage.
So I have modified the code a bit for android 10 especially
string logDir = "Documents/MyApp_Data/logs/";
Context context = MyApplication.Context;
ContentValues values = new ContentValues();
values.Put(MediaStore.MediaColumns.DisplayName, filename);
values.Put(MediaStore.MediaColumns.MimeType, "text/plain"); //file extension, will automatically add to file
values.Put(MediaStore.MediaColumns.RelativePath, logDir);
var uri = context.ContentResolver.Insert(MediaStore.Files.GetContentUri("external"), values);
Stream outputStream = context.ContentResolver.OpenOutputStream(uri, "rw");
outputStream.Write(Encoding.UTF8.GetBytes(message));
outputStream.Close();
The above code is working for android 10 but it is creating multiple log files instead I want to update the file if the file already exists. I am not getting a way to check if the file exists then append new data in the existing file. Can someone please let me know? The above code is in Xamarin android but if you have any suggestion that will work in android then I will convert that code to Xamarin android
Thanks in advance
This code corrects (especially words' upper/lower cases) vaibhav ones and use blackapps suggestion to include text append. Can write txt or json.
Good to write text in persistent folders (e.g. /storage/self/Downloads) without user interaction on Android 10+ (actually not tested on 11, but should work).
// filename can be a String for a new file, or an Uri to append it
fun saveTextQ(ctx: Context,
relpathOrUri: Any,
text: String,
dir: String = Environment.DIRECTORY_DOWNLOADS):Uri?{
val fileUri = when (relpathOrUri) {
is String -> {
// create new file
val mime = if (relpathOrUri.endsWith("json")) "application/json"
else "text/plain"
val values = ContentValues()
values.put(MediaStore.MediaColumns.DISPLAY_NAME, relpathOrUri)
values.put(MediaStore.MediaColumns.MIME_TYPE, mime) //file extension, will automatically add to file
values.put(MediaStore.MediaColumns.RELATIVE_PATH, dir)
ctx.contentResolver.insert(MediaStore.Files.getContentUri("external"), values) ?: return null
}
is Uri -> relpathOrUri // use given Uri to append existing file
else -> return null
}
val outputStream = ctx.contentResolver.openOutputStream(fileUri, "wa") ?: return null
outputStream.write(text.toByteArray(charset("UTF-8")))
outputStream.close()
return fileUri // return Uri to then allow append
}
I added files to Documents/MyExcelsFolder by using ContentResolver.insert and then also added new file to Documents/MyExcelsFolder folder by another app (for ex. FileManager)
Then I try to get all files from the MyExcelsFolder folder
fun getAppFiles(context: Context): List<AppFile> {
val appFiles = mutableListOf<AppFile>()
val contentResolver = context.contentResolver
val columns = mutableListOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DATE_ADDED,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.MIME_TYPE
).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
add(
MediaStore.MediaColumns.RELATIVE_PATH
)
}
}.toTypedArray()
val extensions = listOf("xls", "xlsx")
val mimes = extensions.map { MimeTypeMap.getSingleton().getMimeTypeFromExtension(it) }
val selection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
"${MediaStore.MediaColumns.RELATIVE_PATH} LIKE ?"
} else {
"${MediaStore.Images.Media.DATA} LIKE ?"
}
val selectionArgs = arrayOf(
"%${Environment.DIRECTORY_DOCUMENTS}/MyExcelsFolder%"
)
contentResolver.query(
MediaStore.Files.getContentUri("external"),
columns,
selection,
selectionArgs,
MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
)?.use { cursor ->
while (cursor.moveToNext()) {
val pathColumnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
val mimeColumnIndex = cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE)
val filePath = cursor.getString(pathColumnIndex)
val mimeType = cursor.getString(mimeColumnIndex)
if (mimeType != null && mimes.contains(mimeType)) {
// handle cursor
appFiles.add(cursor.toAppFile())
} else {
// need to check extension, because the Mime Type is null
val extension = File(filePath).extension
if (extensions.contains(extension)) {
// handle cursor
appFiles.add(cursor.toAppFile())
}
}
}
}
return appFiles
}
fun Cursor.toAppFile(): AppFile {
val cursor = this
val idColumnIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns._ID)
val nameColumnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
val mimeColumnIndex = cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE)
val pathColumnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
val id = cursor.getLong(idColumnIndex)
val uri = ContentUris.withAppendedId(MediaStore.Files.getContentUri("external"), id)
val fileDisplayName = cursor.getString(nameColumnIndex)
val filePath = cursor.getString(pathColumnIndex)
var mimeType = cursor.getString(mimeColumnIndex)
val relativePath = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.RELATIVE_PATH))
} else {
null
}
var type = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
if (type == null) {
type = File(filePath).extension
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(type)
}
return AppFile(
id = id,
uri = uri,
absolutePath = filePath,
name = fileDisplayName,
mimeType = mimeType,
extension = type,
relativePath = relativePath
)
}
And in result there are only files from ContentResolver added by insert command, and there no files copied by FileManager. How to see all files in cursor?
Operation system: Android 10 (Q) (API level 29)
Target API version: api 29
Starting from Android 10 there is a new storage access model in action which is called Scoped Storage and it is much more restrictive. In short:
Your app can always access its own directories.
Your app can write (with help of ContentResolver.insert) to the shared media collections and can read only files created by your app from them. You can access other apps files from these collections by requesting the READ_EXTERNAL_STORAGE permission.
Your app can access other files and directories by using file or directory system pickers.
It's a bit weird and looks like a bug that you are able to access xls files through the MediaStore.Files collection. The documentation says
The media store also includes a collection called MediaStore.Files.
Its contents depend on whether your app uses scoped storage, available
on apps that target Android 10 or higher:
If scoped storage is enabled, the collection shows only the photos,
videos, and audio files that your app has created.
If scoped storage
is unavailable or not being used, the collection shows all types of
media files.
But anyway you still are not able to access files created by other apps as stated above.
So depending on your use case there are a few options to go:
As accessing files through the MediaStore.Files works for you now, you can try to request READ_EXTERNAL_STORAGE permission as shown in this table to get a non-filtered access to media collections. But I would expect such way to work unreliably on different devices and/or expect to stop it working with the new updates, because the media collections are supposed to be used for media files only.
You can use ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE to show a file/directory picker to user and get access to a file or the whole directory tree. Please check the restrictions of this method also. I'd say this is the preferable way to go.
Android 10 allows you to temporarily opt-out from the scoped storage by using the android:requestLegacyExternalStorage flag. But Android 11 is out already and this flag does not have any effect in it.
You can request the new MANAGE_EXTERNAL_STORAGE permission and then request a special white-listing by user to access all files. That's how the file managers work now. This feature is available starting from Android 11, so you'll likely to use the opt-out flag for Android 10. Also be sure to check the restrictions and Google Play policies on using this feature if you are going to publish your app there.
According to the docs file path access is granted in Android R:
Starting in Android 11, apps that have the READ_EXTERNAL_STORAGE permission can read a device's media files using direct file paths and native libraries. This new capability allows your app to work more smoothly with third-party media libraries.
The problem is that I can't get the file path from MediaStore, so how are we supposed to read a file path that we can't access/retrieve? Is there a way, I'm not aware of, that we can get the file path from MediaStore?
Furthermore, the docs say the following:
All Files Access
Some apps have a core use case that requires broad file access, such as file management or backup & restore operations. They can get All Files Access by doing the following:
Declare the MANAGE_EXTERNAL_STORAGE permission.
Direct users to a system settings page where they can enable the Allow access to manage all files option for your app.
This permission grants the following:
Read access and write access to all files within shared storage.
Access to the contents of the MediaStore.Files table.
But I do not need all file access, I only want the user to select a video from MediaStore and pass the file path to FFmpeg(it requires a file path). I know that I can no longer use the _data column to retrieve a file path.
Please note:
I know a Uri is returned from MediaStore and does not point to a file.
I know that I can copy the file to my application directory and pass that to FFmpeg, but I could do that before Android R.
I can not pass FileDescriptor to FFmpeg and I can not use /proc/self/fd/ (I get /proc/7828/fd/70: Permission denied when selecting a file from the SD Card), have a look at this issue.
So what am I supposed to do, am I missing something? What was meant with can read a device's media files using direct file paths and native libraries?
After asking a question on issuetracker, I've come to the following conclusions:
On Android R, the File restrictions that were added in Android Q is removed. So we can once again access File objects.
If you are targeting Android 10 > and you want to access/use file paths, you will have to add/keep the following in your manifest:
android:requestLegacyExternalStorage="true"
This is to ensure that file paths are working on Android 10(Q). On Android R this attribute will be ignored.
Don't use DATA column for inserting or updating into Media Store, use DISPLAY_NAME and RELATIVE_PATH, here is an example:
ContentValues valuesvideos;
valuesvideos = new ContentValues();
valuesvideos.put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/" + "YourFolder");
valuesvideos.put(MediaStore.Video.Media.TITLE, "SomeName");
valuesvideos.put(MediaStore.Video.Media.DISPLAY_NAME, "SomeName");
valuesvideos.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
valuesvideos.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
valuesvideos.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
valuesvideos.put(MediaStore.Video.Media.IS_PENDING, 1);
ContentResolver resolver = getContentResolver();
Uri collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
Uri uriSavedVideo = resolver.insert(collection, valuesvideos);
You can no longer use the ACTION_OPEN_DOCUMENT_TREE or the ACTION_OPEN_DOCUMENT intent action to request that the user select individual files from Android/data/,Android/obb/and all sub-directories.
It is recommended to only use File objects when you need to perform "seeking", like when using FFmpeg, for example.
You can only use the data column to access files that are on the disk. You should handle I/O Exceptions accordingly.
If you want to access a File or want a file path from a Uri that was returned from MediaStore, I've created a library that handles all the exceptions you might get. This includes all files on the disk, internal and removable disk. When selecting a File from Dropbox, for example, the File will be copied to your applications directory where you have full access, the copied file path will then be returned.
If you are targeting to Android 11 API, you cannot directly get access to the file paths, as there are many restrictions in API 30(Android R). As scoped storage API was introduced in Android 10(API 29), the storage is now divided into scoped storage (private storage) and shared storage (public storage). Scoped storage is a kind you can only have access to the files that are created in your scoped storage directory(i.e. /Android/data/ or /Android/media/<your-package-name>). You cannot access files from shared storage (i.e. internal storage/external SD card storage etc.)
The shared storage is again further divided into Media and Download collection. Media collection stores Image, Audio and Video files. Download collection would take care of non-media files.
To learn in more details about scoped storage and shared storage refer this link: Scoped Storage in Android 10 & Android 11 .
If you are dealing with Media files (i.e. Images, Videos, Audio) you can get the file path by Using Media Store API that having support to API 30(Android 11). and If you are dealing with non-media files(i.e. documents and other files) you can get the file path by using file Uri.
Note: If you are using the file or Uri util classes (such as RealPathUtil, FilePathUtils etc.) to get the file path, here you can get the desired file path but you cannot read that file, as it will throw an exception of Read Access (as Permission denied) in Android 11, as you cannot read the files that are created by another application.
So to achieve this scenario of getting the file path in Android 11(API 30), It a recommended to copy the file into the cache directory of your application using File Uri and get the path of the file access from cache directory.
Here in my scenario I have used both APIs to get the file access in Android 11. To get the file path of the media files (i.e. Images, Videos, Audio), I've used the Media Store API (Refer this link: Media Store API Example - Access media files from shared storage ), and to get the file path of the non-media files (i.e. Documents and other files), I've used fileDescriptor.
File Descriptor Example:
I have created the system dialog file picker to pick the file.
private fun openDocumentAction() {
val mimetypes = arrayOf(
"application/*", //"audio/*",
"font/*", //"image/*",
"message/*",
"model/*",
"multipart/*",
"text/*"
)
// you can customize the mime types as per your choice.
// Choose a directory using the system's file picker.
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
//type = "application/pdf" //only pdf files
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, mimetypes)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
// Optionally, specify a URI for the directory that should be opened in
// the system file picker when it loads.
//putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
startActivityForResult(intent, RC_SAF_NON_MEDIA)
}
And handled the result of file picker in onActivityResult method of the activity. Get the file URI at here.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
RC_SAF_NON_MEDIA -> {
//document selection by SAF(Storage Access Framework) for Android 11
if (resultCode == RESULT_OK) {
// The result data contains a URI for the document or directory that
// the user selected.
data?.data?.also { uri ->
//Permission needed if you want to retain access even after reboot
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
// Perform operations on the document using its URI.
val path = makeFileCopyInCacheDir(uri)
Log.e(localClassName, "onActivityResult: path ${path.toString()} ")
}
}
}
}
}
Pass the file URI to the below method to get the file path. This method will create a file object at cache directory of your application and from that location you can easily get Read access to that file.
private fun makeFileCopyInCacheDir(contentUri :Uri) : String? {
try {
val filePathColumn = arrayOf(
//Base File
MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.TITLE,
MediaStore.Files.FileColumns.DATA,
MediaStore.Files.FileColumns.SIZE,
MediaStore.Files.FileColumns.DATE_ADDED,
MediaStore.Files.FileColumns.DISPLAY_NAME,
//Normal File
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.MediaColumns.DISPLAY_NAME
)
//val contentUri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(mediaUrl))
val returnCursor = contentUri.let { contentResolver.query(it, filePathColumn, null, null, null) }
if (returnCursor!=null) {
returnCursor.moveToFirst()
val nameIndex = returnCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
val name = returnCursor.getString(nameIndex)
val file = File(cacheDir, name)
val inputStream = contentResolver.openInputStream(contentUri)
val outputStream = FileOutputStream(file)
var read = 0
val maxBufferSize = 1 * 1024 * 1024
val bytesAvailable = inputStream!!.available()
//int bufferSize = 1024;
val bufferSize = Math.min(bytesAvailable, maxBufferSize)
val buffers = ByteArray(bufferSize)
while (inputStream.read(buffers).also { read = it } != -1) {
outputStream.write(buffers, 0, read)
}
inputStream.close()
outputStream.close()
Log.e("File Path", "Path " + file.path)
Log.e("File Size", "Size " + file.length())
return file.absolutePath
}
} catch (ex: Exception) {
Log.e("Exception", ex.message!!)
}
return contentUri.let { UriPathUtils().getRealPathFromURI(this, it).toString() }
}
Note: You can use this method to get file path for both media files (Images, Videos, Audio) and non-media files (Documents and other files) as well. Just need to pass a file Uri.
For getting path, i'm coping file with fileDescriptor to new path & i use that path.
Finding File Name:
private static String copyFileAndGetPath(Context context, Uri realUri, String id) {
final String selection = "_id=?";
final String[] selectionArgs = new String[]{id};
String path = null;
Cursor cursor = null;
try {
final String[] projection = {"_display_name"};
cursor = context.getContentResolver().query(realUri, projection, selection, selectionArgs,
null);
cursor.moveToFirst();
final String fileName = cursor.getString(cursor.getColumnIndexOrThrow("_display_name"));
File file = new File(context.getCacheDir(), fileName);
FileUtils.saveAnswerFileFromUri(realUri, file, context);
path = file.getAbsolutePath();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null)
cursor.close();
}
return path;
}
Copy With File Descriptor:
fun saveAnswerFileFromUri(uri: Uri, destFile: File?, context: Context) {
try {
val pfd: ParcelFileDescriptor =
context.contentResolver.openFileDescriptor(uri, "r")!!
if (pfd != null) {
val fd: FileDescriptor = pfd.getFileDescriptor()
val fileInputStream: InputStream = FileInputStream(fd)
val fileOutputStream: OutputStream = FileOutputStream(destFile)
val buffer = ByteArray(1024)
var length: Int
while (fileInputStream.read(buffer).also { length = it } > 0) {
fileOutputStream.write(buffer, 0, length)
}
fileOutputStream.flush()
fileInputStream.close()
fileOutputStream.close()
pfd.close()
}
} catch (e: IOException) {
Timber.w(e)
}
}
As getExternalStoragePublicDirectory has been deprecated in Android Q, and the recommendation is to use other means. then how can we specify that we want to store the generated photos from a camera app into the DCIM folder, or a custom sub-folder within the DCIM?
The documentation states that the next 3 options are the new preferred alternatives:
Context#getExternalFilesDir(String)
Intent#ACTION_OPEN_DOCUMENT
MediaStore
Option 1 is out of the questions as it would mean that the photos get deleted if the app gets uninstalled.
Option 2 is also not a choice, as it would require the user to pick the location through the SAF file explorer.
We are left with option 3, the MediaStore; but at the time of this question there is no documentation on how to use it as a replacement for getExternalStoragePublicDirectory in Android Q.
Based on the docs, use DCIM/... for the RELATIVE_PATH, where ... is whatever your custom subdirectory would be. So, you would wind up with something like this:
val resolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "CuteKitten001")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/PerracoLabs")
}
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
resolver.openOutputStream(uri).use {
// TODO something with the stream
}
Note that since RELATIVE_PATH is new to API Level 29, you would need to use this approach on newer devices and use getExternalStoragePublicDirectory() on older ones.
#CommonsWare answer is amazing. But for those who want it in Java, you need to try this:
ContentResolver resolver = context.getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
As per the suggestion of #SamChen the code should look like this for text files:
Uri uri = resolver.insert(MediaStore.Files.getContentUri("external"), contentValues);
Because we wouldn't want txt files lingering in the Images folder.
So, the place where I have mimeType, you enter the mime type you want. For example if you wanted txt (#Panache) you should replace mimeType with this string: "text/plain". Here is a list of mime types: https://www.freeformatter.com/mime-types-list.html
Also, where I have the variable name, you replace it with the name of the file in your case.
Apps targeting Android Q - API 29+ disabled storage access by default due to security issues. If you want to enable it to add the following attribute in the AndroidManifest.xml:
<manifest ... >
<!-- This attribute is "false" by default for Android Q or higher -->
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>
then you have to use getExternalStorageDirectory() instead of getExternalStoragePublicDirectory().
Example: If you want to create a directory in the internal storage if not exists.
File mediaStorageDir = new File(Environment.getExternalStorageDirectory() + "/SampleFolder");
// Create the storage directory if it does not exist
if (! mediaStorageDir.exists()){
if (! mediaStorageDir.mkdirs()){
Log.d("error", "failed to create directory");
}
}
import android.content.ContentValues
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
class MyActivity : AppCompatActivity() {
#RequiresApi(Build.VERSION_CODES.Q)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_mine)
val editText: EditText = findViewById(R.id.edt)
val write: Button = findViewById(R.id.Output)
val read: Button = findViewById(R.id.Input)
val textView: TextView = findViewById(R.id.textView)
val resolver = this.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "myDoc1")
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
put(MediaStore.MediaColumns.RELATIVE_PATH, "Documents")
}
val uri: Uri? = resolver.insert(MediaStore.Files.getContentUri("external"), contentValues)
Log.d("Uri", "$uri")
write.setOnClickListener {
val edt : String = editText.text.toString()
if (uri != null) {
resolver.openOutputStream(uri).use {
it?.write("$edt".toByteArray())
it?.close()
}
}
}
read.setOnClickListener {
if (uri != null) {
resolver.openInputStream(uri).use {
val data = ByteArray(50)
it?.read(data)
textView.text = String(data)
}
}
}
}
}
Here, I am storing a text file in phone's Document folder by writing text into edit text and by clicking button 'Write' it will save the file with the text written.
On clicking button 'Read' it will bring the text from that file and then display it in the text view.
It will not run on devices that are below android Q or android 10 as RELATIVE_PATH can only be used in these versions.
For Xamarin.Android the following code from a published project could help:
Java.IO.File jFolder;
if ((int)Android.OS.Build.VERSION.SdkInt >= 29)
{
jFolder = new Java.IO.File(Android.App.Application.Context.GetExternalFilesDir(Environment.DirectoryDcim), "Camera");
}
else
{
jFolder = new Java.IO.File(Environment.GetExternalStoragePublicDirectory(Environment.DirectoryDcim), "Camera");
}
if (!jFolder.Exists())
jFolder.Mkdirs();
var filename = GenerateJpgFileName();
var jFile = new Java.IO.File(jFolder, filename);
var fullFilename = jFile.AbsoluteFile.ToString();
using (var output = new System.IO.FileStream(fullFilename, System.IO.FileMode.Create))
{
outputBitmap.Compress(Bitmap.CompressFormat.Jpeg, 90, output);
output.Close();
}
Not using permissions in Android, it's done in from the shared project using Xamarin.Essentials.
If you want to save your file in a app specific external storage, yes you can use context.getExternalFilesDir(). Many answers point out that.
However, this is not the answer of this question because getExternalFilesDir() is app specific external storage, getExternalStoragePublicDirectory() is shared storage.
For example, you want to save a downloaded pdf file to "Shared" Download directory. How do you do that ? For api 29 and above, you can do that without no permission.
For api 28 and below, you need getExternalStoragePublicDirectory() method but it is deprecated. What if you don't want to use that deprecated method? Then you can use SAF file explorer(Intent#ACTION_OPEN_DOCUMENT). As said in the question, this requires the user to pick the location manually.
This is what Google wants exactly. To improve user privacy, direct access to shared/external storage devices is deprecated.
When an app targets Build.VERSION_CODES.Q, the path returned from this
method is no longer directly accessible to apps. Apps can continue to
access content stored on shared/external storage by migrating to
alternatives such as Context#getExternalFilesDir(String), MediaStore,
or Intent#ACTION_OPEN_DOCUMENT.
Details are given in the following link:
https://developer.android.com/reference/android/os/Environment#getExternalStorageDirectory()
Generally I used this way :
var data: File =Environment.getExternalStoragePublicDirectory
(Environment.DIRECTORY_DOWNLOADS)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
data = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!!
}