Android 11 (R) file path access - android

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)
}
}

Related

How to write a JSON file to a specific location using the Media Store API in Android

Having invoked a directory selector on Android with:
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
)
activity?.startActivityForResult(intent, REQUEST_CODE_FOLDER_PERMISSION)
And having obtained the URI of said route in onActivityResult(), being the URI of the form (example in case of having chosen a folder named backup in the root of the external storage):
content://com.android.externalstorage.documents/tree/primary:backup
At this point, how do you write a file to that location? After researching various answers on how to write files using the Media Store API, all the examples I've seen use constants to refer to already existing media directories, but in my case I want to create a new document (which is a JSON file) in the directory chosen by the user.
You will not use the MediaStore to save a file if you obtained an uri using ACTION_OPEN_DOCUMENT_TREE to get permission for a folder.
Just continue to use Storage Access Framework and implement DocumentFile.createFile().
Pretty basic exercise for learning SAF.
If you want to use the MediaStore to save a file then you do not have the user to select a folder first.
Thanks to #CommonsWare for pointing me in the right direction:
var outputStream: OutputStream? = null
try {
val uri = Uri.parse(path)
val document = DocumentFile.fromTreeUri(context, uri)
val file = document?.createFile(mimeType, filename)
?: throw Exception("Created file is null, cannot continue")
val fileUri = file.uri
val contentResolver = context.contentResolver
outputStream = contentResolver.openOutputStream(fileUri)
val bytes = content.toByteArray()
outputStream?.write(bytes)
outputStream?.flush()
} catch (e: Exception) {
// Handle error
} finally {
outputStream?.close()
}

Video path in Android 11 [duplicate]

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)
}
}

Saving media file path or URI to SQLite and getting it, best practice

My goal is to:
Save media file to External Storage (in my case it's photo).
Get file path or URI of saved data.
Save it to SQLite (either file path or content URI or smth else).
Be able to get correct URI to this content at any point in the future.
It's very similar to what other very popular application do - they create their directory in 'Pictures' folder and store there photos and use them in their applications while they're also available for viewing using gallery/file explorer etc.
As I understand recommended way to save media content (image, f.e.) is to use MediaStore API and as a result I get content URI, which I can use later.
But then I read that these content URIs might be changed after re-scan of Media happens, so it looks it's not reliable. (For example if SD card is used and it's taken out and inserted again)
At the same time usage of absolute file paths is not recommended and there's tendency to deprecate APIs which use absolute file paths to work with External Storage. So it doesn't look reliable either.
I can only imagine the following solution:
Use unique auto-generated file name while saving (like UUID).
When I need to get content URI (f.e. want to render photo in ImageView) - I can use ContentResolver and search for content URI using file name filter.
Problem with this approach is that I have a lot of photos (gallery) and querying it using ContentResolver can affect performance significantly.
I feel like I'm over complicating things and missing something.
You are indeed overcomplicating things.
Store file to the needed folder in the filesystem(it is better to name the folder under your app name)
Store this path or URI path - whatever you like. (Do not hardcode passes though in your app - device vendors may have different base paths in their devices)
As long as the folder is named the same and files in it named the same(as in your db) - you will be able to access them even if the sdcard was taken out and then put back in.
There are possible complications after reindexing - but for the eight years I work as Android dev I encountered it only once, thus you can easily ignore this stuff.
If you want to have more control over what you store and want to limit access to it - store data into the inner storage of your app - this way you will be 100% sure of where the data is and that it is not tampered with.
Starting from Android 10 you have scoped storage - it is like internal storage but it may be even on an external sdcard.
Here is a small overview of possible storage locations.
And don't overthink it too much - it is a default usecase of the phone and it works just as you would expect - pretty ok and pretty stable.
first, you have to apply for external storage permission in manifest and Runtime Permission Also.
after creating a directory for saving an image in this directory.
you have to also add file provider in XML and code side because it's required.
now it's time to code check my code for saving an image in the folder also these image in the gallery and get the path from a file path.
convert URI to bitmap
http://prntscr.com/10dpvjj
save image function from getting bitmap
private String save(Bitmap bitmap) {
File save_path = null;
if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) {
try {
File sdCard = Environment.getExternalStorageDirectory();
File dir = new File(sdCard.getAbsolutePath() + "/SaveDirectory");
dir.mkdirs();
File file = new File(dir, "DirName_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(Calendar.getInstance().getTime()) + ".png");
save_path = file;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
FileOutputStream f = null;
f = new FileOutputStream(file);
MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, null, null);
if (f != null) {
f.write(baos.toByteArray());
f.flush();
f.close();
}
} catch (Exception e) {
// TODO: handle exception
}
Share(save_path); // call your Function Store into database
Log.e("PathOFExec----", "save: " + save_path);
}
get store image location into your database if you wish
private void Share(File savePath) {
if (savePath != null) {
Uri uri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", savePath);
Intent share = new Intent(Intent.ACTION_SEND);
share.setType("image/*");
share.putExtra(Intent.EXTRA_TEXT, "TextDetail");
share.putExtra(Intent.EXTRA_STREAM, uri);
context.startActivity(Intent.createChooser(share, "Share Image!"));
//after getting URI you can store the image into SQLite databse for get uri
}
}
I would recommend using Intent.ACTION_OPEN_DOCUMENT for your demand.
1. Create Photo Picking Intent:
val REQUEST_CODE_PICK_PHOTO = 1
fun pickAndSavePhoto(requestCode: Int) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.type = "image/*"
startActivityForResult(intent, requestCode)
}
2. Handle Result:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_PICK_PHOTO && resultCode == RESULT_OK) {
val imageUri = data!!.data!!
//save this uri to your database as String -> imageUri.toString()
}
}
3. Get Image back and Display on ImageView:
fun getBitmapFromUri(context: Context, imageUri: Uri): Bitmap? { //uri is just an address, image may be deleted any time, if so returns null
val bitmap: Bitmap
return try {
val inputStream = context.contentResolver.openInputStream(imageUri)
inputStream.use {
bitmap = BitmapFactory.decodeStream(it)
}
bitmap
} catch (e: Exception) {
Log.e("getBitmapFromUri()", "Image not found.")
null
}
}
val bitmap = getBitmapFromUri(context, imageUri) //get uri String from database and convert it to uri -> uriString.toUri()
if (bitmap != null) {
imageView.setImageBitmap(bitmap)
}
Only ACTION_OPEN_DOCUMENT can access file uri permanently:
Android Retrieve Image by Intent Uri Failed: "has no access to content..."
Demo: https://www.youtube.com/watch?v=LFfWnt77au8

Why ContentResolver does not see added files by another app?

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.

getExternalStoragePublicDirectory deprecated in Android Q

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)!!
}

Categories

Resources