I'am using the storage access framework to download images to the external sd card. The problem is that these images do not appear in the gallery. I've tried to inform the Android media scanner using an intent by sending the DocumentFile uri, but that doesn't seam to work. Here is how I try to inform the media scanner that a new image is added:
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(documentFile.getUri());
sendBroadcast(intent);
Is there another way to inform Android that a new image was added? (I've already tried the methods described here, but I couldn't get the real path using those methods)
I've now found a solution. I'am using the following method to get the file path from the DocumentFile. When sending this path to the media scanner the file is scanned correctly:
private static Object[] volumes;
public static Uri getDocumentFileRealPath(Context context, DocumentFile documentFile) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
final String docId = DocumentsContract.getDocumentId(documentFile.getUri());
final String[] split = docId.split(":");
final String type = split[0];
if (type.equalsIgnoreCase("primary")) {
File file = new File (Environment.getExternalStorageDirectory(), split[1]);
return Uri.fromFile(file);
} else {
if (volumes == null) {
StorageManager sm=(StorageManager)context.getSystemService(Context.STORAGE_SERVICE);
Method getVolumeListMethod = sm.getClass().getMethod("getVolumeList", new Class[0]);
volumes = (Object[])getVolumeListMethod.invoke(sm);
}
for (Object volume : volumes) {
Method getUuidMethod = volume.getClass().getMethod("getUuid", new Class[0]);
String uuid = (String)getUuidMethod.invoke(volume);
if (uuid != null && uuid.equalsIgnoreCase(type))
{
Method getPathMethod = volume.getClass().getMethod("getPath", new Class[0]);
String path = (String)getPathMethod.invoke(volume);
File file = new File (path, split[1]);
return Uri.fromFile(file);
}
}
}
return null;
}
Related
I am trying to upload document from my app.
Everything working fine but when i choose file from drive.
data=Intent { act=android.intent.action.VIEW dat=content://com.google.android.apps.docs.storage.legacy/enc=ckpgt5KcEEF_JYniJQafRV_5pEnu_D5UAI1WF-Lu6h2Z_Vw4
(has extras) }}
Can any body know how to handle this file.
I had already handle all files and images only facing problem with google drive files.
I am getting this content://com.google.android.apps.docs.storage.legacy/enc=ckpgt5KcEEF_JYniJQafRV_5pEnu_D5UAI1WF-Lu6h2Z_Vw4 in intent data Uri.
Handle Uri received by Google-Drive files when selected through file chooser.
as stated earlier it receives Virtual File Uri.
I found this sample code simple and easy to understand.
the given code sample worked for me .hope it works in your case.
1.So detect this Uri is received by google drive.
public static File getFileFromUri(final Context context, final Uri uri) throws Exception {
if (isGoogleDrive(uri)) // check if file selected from google drive
{
return saveFileIntoExternalStorageByUri(context, uri);
}else
// do your other calculation for the other files and return that file
return null;
}
public static boolean isGoogleDrive(Uri uri)
{
return "com.google.android.apps.docs.storage.legacy".equals(uri.getAuthority());
}
2.if yes,the uri is stored to external path(here its root directory u can change it according to your need) and the file with that uri is created.
public static File saveFileIntoExternalStorageByUri(Context context, Uri uri) throws
Exception {
InputStream inputStream = context.getContentResolver().openInputStream(uri);
int originalSize = inputStream.available();
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
String fileName = getFileName(context, uri);
File file = makeEmptyFileIntoExternalStorageWithTitle(fileName);
bis = new BufferedInputStream(inputStream);
bos = new BufferedOutputStream(new FileOutputStream(
file, false));
byte[] buf = new byte[originalSize];
bis.read(buf);
do {
bos.write(buf);
} while (bis.read(buf) != -1);
bos.flush();
bos.close();
bis.close();
return file;
}
public static String getFileName(Context context, Uri uri)
{
String result = null;
if (uri.getScheme().equals("content")) {
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
try {
if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
}
} finally {
cursor.close();
}
}
if (result == null) {
result = uri.getPath();
int cut = result.lastIndexOf('/');
if (cut != -1) {
result = result.substring(cut + 1);
}
}
return result;
}
public static File makeEmptyFileIntoExternalStorageWithTitle(String title) {
String root = Environment.getExternalStorageDirectory().getAbsolutePath();
return new File(root, title);
}
Note:Here the virtual file is retrieved from Intent getData() and used in context.getContentResolver().openInputStream(intent.getData()), this will return an InputStream. It's handle to get selected file from google drive.
for more info go through this link
I think you are getting Virtual File Uri from google drive
Read more about Virtual Files
FROM DOCS
Virtual Files
Android 7.0 adds the concept of virtual files to the Storage Access Framework. The virtual files feature allows your DocumentsProvider to return document URIs that can be used with an ACTION_VIEW intent even if they don't have a direct bytecode representation. Android 7.0 also allows you to provide alternate formats for user files, virtual or otherwise
Now question is how to check the the Uri is VirtualFile or not
You can find sample code from DOCS Open virtual files
first check that Uri is VirtualFile or not
private boolean isVirtualFile(Uri uri) {
if (!DocumentsContract.isDocumentUri(this, uri)) {
return false;
}
Cursor cursor = getContentResolver().query(
uri,
new String[] { DocumentsContract.Document.COLUMN_FLAGS },
null, null, null);
int flags = 0;
if (cursor.moveToFirst()) {
flags = cursor.getInt(0);
}
cursor.close();
return (flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0;
}
The following code snippet shows how to check whether a virtual file can be represented as an image, and if so, gets an input stream from the virtual file
private InputStream getInputStreamForVirtualFile(Uri uri, String mimeTypeFilter)
throws IOException {
ContentResolver resolver = getContentResolver();
String[] openableMimeTypes = resolver.getStreamTypes(uri, mimeTypeFilter);
if (openableMimeTypes == null ||
openableMimeTypes.length < 1) {
throw new FileNotFoundException();
}
return resolver
.openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null)
.createInputStream();
}
For more information of Virtual Files you can read below article
Virtual Files FAQ
Open files using storage access framework
An Android Storage Access Framework Example
I am trying to upload document from my app.
Everything working fine but when i choose file from drive.
data=Intent { act=android.intent.action.VIEW dat=content://com.google.android.apps.docs.storage.legacy/enc=ckpgt5KcEEF_JYniJQafRV_5pEnu_D5UAI1WF-Lu6h2Z_Vw4
(has extras) }}
Can any body know how to handle this file.
I had already handle all files and images only facing problem with google drive files.
I am getting this content://com.google.android.apps.docs.storage.legacy/enc=ckpgt5KcEEF_JYniJQafRV_5pEnu_D5UAI1WF-Lu6h2Z_Vw4 in intent data Uri.
Handle Uri received by Google-Drive files when selected through file chooser.
as stated earlier it receives Virtual File Uri.
I found this sample code simple and easy to understand.
the given code sample worked for me .hope it works in your case.
1.So detect this Uri is received by google drive.
public static File getFileFromUri(final Context context, final Uri uri) throws Exception {
if (isGoogleDrive(uri)) // check if file selected from google drive
{
return saveFileIntoExternalStorageByUri(context, uri);
}else
// do your other calculation for the other files and return that file
return null;
}
public static boolean isGoogleDrive(Uri uri)
{
return "com.google.android.apps.docs.storage.legacy".equals(uri.getAuthority());
}
2.if yes,the uri is stored to external path(here its root directory u can change it according to your need) and the file with that uri is created.
public static File saveFileIntoExternalStorageByUri(Context context, Uri uri) throws
Exception {
InputStream inputStream = context.getContentResolver().openInputStream(uri);
int originalSize = inputStream.available();
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
String fileName = getFileName(context, uri);
File file = makeEmptyFileIntoExternalStorageWithTitle(fileName);
bis = new BufferedInputStream(inputStream);
bos = new BufferedOutputStream(new FileOutputStream(
file, false));
byte[] buf = new byte[originalSize];
bis.read(buf);
do {
bos.write(buf);
} while (bis.read(buf) != -1);
bos.flush();
bos.close();
bis.close();
return file;
}
public static String getFileName(Context context, Uri uri)
{
String result = null;
if (uri.getScheme().equals("content")) {
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
try {
if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
}
} finally {
cursor.close();
}
}
if (result == null) {
result = uri.getPath();
int cut = result.lastIndexOf('/');
if (cut != -1) {
result = result.substring(cut + 1);
}
}
return result;
}
public static File makeEmptyFileIntoExternalStorageWithTitle(String title) {
String root = Environment.getExternalStorageDirectory().getAbsolutePath();
return new File(root, title);
}
Note:Here the virtual file is retrieved from Intent getData() and used in context.getContentResolver().openInputStream(intent.getData()), this will return an InputStream. It's handle to get selected file from google drive.
for more info go through this link
I think you are getting Virtual File Uri from google drive
Read more about Virtual Files
FROM DOCS
Virtual Files
Android 7.0 adds the concept of virtual files to the Storage Access Framework. The virtual files feature allows your DocumentsProvider to return document URIs that can be used with an ACTION_VIEW intent even if they don't have a direct bytecode representation. Android 7.0 also allows you to provide alternate formats for user files, virtual or otherwise
Now question is how to check the the Uri is VirtualFile or not
You can find sample code from DOCS Open virtual files
first check that Uri is VirtualFile or not
private boolean isVirtualFile(Uri uri) {
if (!DocumentsContract.isDocumentUri(this, uri)) {
return false;
}
Cursor cursor = getContentResolver().query(
uri,
new String[] { DocumentsContract.Document.COLUMN_FLAGS },
null, null, null);
int flags = 0;
if (cursor.moveToFirst()) {
flags = cursor.getInt(0);
}
cursor.close();
return (flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0;
}
The following code snippet shows how to check whether a virtual file can be represented as an image, and if so, gets an input stream from the virtual file
private InputStream getInputStreamForVirtualFile(Uri uri, String mimeTypeFilter)
throws IOException {
ContentResolver resolver = getContentResolver();
String[] openableMimeTypes = resolver.getStreamTypes(uri, mimeTypeFilter);
if (openableMimeTypes == null ||
openableMimeTypes.length < 1) {
throw new FileNotFoundException();
}
return resolver
.openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null)
.createInputStream();
}
For more information of Virtual Files you can read below article
Virtual Files FAQ
Open files using storage access framework
An Android Storage Access Framework Example
On Android N, i am getting an exception. It is a known issue per the documentation, which asks me to use ContentResolver.openFileDescriptor()
https://developer.android.com/reference/android/app/DownloadManager.html#COLUMN_LOCAL_FILENAME
I am not sure how to use. Where is the ContentResolver object here that I can use to get the filename? I never used it. So, I will appreciate any help.
08-04 11:20:59.765 7010 7290 W System.err: java.lang.SecurityException: COLUMN_LOCAL_FILENAME is deprecated; use ContentResolver.openFileDescriptor() instead
08-04 11:20:59.765 7010 7290 W System.err: at android.app.DownloadManager$CursorTranslator.getString(DownloadManager.java:1499)
Here is my code.
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(id);
Cursor cursor = downloadManager.query(query);
final String downloadFilePath = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME));
cursor.close();
I tried the downlaodManager.getFileUri, but isn't what I am looking for. Appreciate any help.
Thanks
The following is working for me now:
String downloadFilePath = null;
String downloadFileLocalUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
if (downloadFileLocalUri != null) {
File mFile = new File(Uri.parse(downloadFileLocalUri).getPath());
downloadFilePath = mFile.getAbsolutePath();
}
I solve this issue by using DownloadManager.COLUMN_LOCAL_URI instead of DownloadManager.COLUMN_LOCAL_FILENAME
DownloadManager.COLUMN_LOCAL_URI returns file path including "file://" so you have to exclude it from your downloadFilePath by using downloadFilePath = downloadFilePath.replace("file://","");
Here is one line solution of this issue:
String downloadFilePath = (c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))).replace("file://","");
Check below complete code of DownloadManager:
DownloadFinishedReceiver.java
public class DownloadFinishedReceiver extends BroadcastReceiver {
#Override
public void onReceive(final Context context, Intent intent) {
String action = intent.getAction();
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action) && intent.getExtras()!=null) {
Bundle extras = intent.getExtras();
DownloadManager.Query q = new DownloadManager.Query();
long downloadId = extras.getLong(DownloadManager.EXTRA_DOWNLOAD_ID);
q.setFilterById(downloadId);
Cursor c = ((DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE)).query(q);
if (c.moveToFirst()) {
int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
if (status == DownloadManager.STATUS_SUCCESSFUL) {
String downloadFilePath = (c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))).replace("file://","");
String downloadTitle = c.getString(c.getColumnIndex(DownloadManager.COLUMN_TITLE));
c.close();
Log.e("DownloadPath", downloadFilePath); // Print DownloadPath in Logcat
Log.e("DownloadTitle", downloadTitle); // Print DownloadTitle in Logcat
} else if (status == DownloadManager.STATUS_FAILED) {
removeTempOnFailure(context, downloadId);
}
}
}
}
// Remove temp/cache data
private void removeTempOnFailure(Context con, long downloadId) {
File cacheFileDir = new File(con.getCacheDir().getAbsolutePath());
for (File f : cacheFileDir.listFiles()) {
if (f.getName().contains(String.valueOf(downloadId))) {
f.delete();
break;
}
}
}
}
Register BroadcastReceiver in AndroidMenifest.xml file:
<receiver
android:name="com.example.receiver.DownloadFinishedReceiver"
android:exported="true"
android:process=".downloadFinished">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter>
</receiver>
Put below method in your Activity and pass appropriate arguments:
/**
*
* #param downloadUrl -> Your file download url
* #param downloadTitle -> Your file title to display in download manager
* #param fileName -> filename with extension like music.mp3 it will store in download folder
* #param hide -> true to hide downloadmanager in status bar, false to display it
* #return -> it will return downloadId
*/
private long downloadFromUrl(String downloadUrl, String downloadTitle, String fileName, boolean hide) {
Uri uri = Uri.parse(downloadUrl);
DownloadManager.Request request = new DownloadManager.Request(uri);
request.setTitle(downloadTitle);
if (hide) {
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
request.setVisibleInDownloadsUi(false);
} else
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
return manager != null ? manager.enqueue(request) : 0;
}
If you are passing hide = true in above method then you have to add following permission in AndroidManifext.xml
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION"/>
Use the getUriForDownloadedFile to get the downloaded Uri.
DownloadManager downloadManager = DownloadManager.class.cast(getSystemService(DOWNLOAD_SERVICE));
Uri uri = downloadManager.getUriForDownloadedFile(id);
String downloadFileLocalUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
Above method may cause CursorIndexOutOfBoundsException in case of empty list of rows.
A Cursor is an object, which is positioned before the first entry. So we should first check if there is any result at all. Here is my example:
int index = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI);
if (cursor.moveToFirst()) {
String downloadFileLocalUri = cursor.getString(index);
if (downloadFileLocalUri != null) {
File mFile = new File(downloadFileLocalUri);
downloadFileName = mFile.getName();
downloadFilePath = mFile.getAbsolutePath();
}
}
cursor.close();
For whoever finds this useful
I had to use DownloadManager.COLUMN_URI
instead of DownloadManager.COLUMN_LOCAL_URI
Here are my results for each respective column
c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)) -> content://downloads/my_downloads/46
c.getString(c.getColumnIndex(DownloadManager.COLUMN_URI)) -> http://test-domain.com/files/encrypted/212125/bsd1e-411cd7-e3229fb-fdddsa12a974.pdf
c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME)) -> http://test-domain.com/files/encrypted/212125/bsd1e-411cd7-e3229fb-fdddsa12a974.pdf
techtinkerer's answer also didn't work for me because I wasn't always getting a file URI, as CommonsWare pointed out, but often a content URI. But there are several ways you can still get the file from the content URI.
1) You can call getContentResolver().openInputStream(myContentUri) to get an input stream, that you can then write to a file you create in external or internal storage.
2) You can call getContentResolver().openFileDescriptor(myContentUri, "r") to open a read-only ParcelFileDescriptor. While you cannot get an absolute file path from a ParcelFileDescriptor, a lot of methods that accept files or URIs also accept ParcelFileDescriptor.
For example I was using DownloadManager to download a PDF then open it with a PdfRenderer, whose constructor accepts a ParcelFileDescriptor:
PdfRenderer renderer;
ParcelFileDescriptor pdfFileDescriptor = getContentResolver().openFileDescriptor(pdfUri, "r");
if (pdfFileDescriptor != null) renderer = new PdfRenderer(pdfFileDescriptor);
pdfFileDescriptor.close();
For another example, BitmapFactory also has a method to decode a ParcelFileDescriptor:
(from https://developer.android.com/guide/topics/providers/document-provider.html)
private Bitmap getBitmapFromUri(Uri uri) throws IOException {
ParcelFileDescriptor parcelFileDescriptor =
getContentResolver().openFileDescriptor(uri, "r");
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
parcelFileDescriptor.close();
return image;
}
So depending on what you want to do, a ParcelFileDescriptor may be all that you need.
You can decrease the targetSdk less than 24 in gradle.Actually this approach is not a recommended approach but we can overcome this error by decreasing the target sdk 23 or anything(but less than 24) as well.
Alright, I've searched and searched and no one has my exact answer, or I missed it. I'm having my users select a directory by:
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, READ_REQUEST_CODE);
In my activity I want to capture the actual path, which seems to be impossible.
protected void onActivityResult(int requestCode, int resultCode, Intent intent){
super.onActivityResult(requestCode, resultCode, intent);
if (resultCode == RESULT_OK) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M){
//Marshmallow
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP){
//Set directory as default in preferences
Uri treeUri = intent.getData();
//grant write permissions
getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
//File myFile = new File(uri.getPath());
DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);
The folder I selected is at:
Device storage/test/
I've tried all of the following ways to get an exact path name, but to no avail.
File myFile = new File (uri.getPath());
//returns: /tree/1AF6-3708:test
treeUri.getPath();
//returns: /tree/1AF6-3708:test/
pickedDir.getName()
//returns: test
pickedDir.getParentFile()
//returns: null
Basically I need to turn /tree/1AF6-3708: into /storage/emulated/0/ or whatever each device calls it's storage location. All other available options return /tree/1AF6-37u08: also.
There are 2 reasons I want to do it this way.
1) In my app I store the file location as a shared preference because it is user specific. I have quite a bit of data that will be downloaded and stored and I want the user to be able to place it where they want, especially if they have an additional storage location. I do set a default, but I want versatility, rather than the dedicated location of:
Device storage/Android/data/com.app.name/
2) In 5.0 I want to enable the user to get read/write permissions to that folder and this seems the only way to do that. If I can get read/write permissions from a string that would fix this issue.
All solutions I've been able to find relate to Mediastore, which doesn't help me exactly. I have to be missing something somewhere or I must have glazed over it. Any help would be appreciated. Thanks.
This will give you the actual path of the selected folder It will work ONLY for files/folders that belong in local storage.
Uri treeUri = data.getData();
String path = FileUtil.getFullPathFromTreeUri(treeUri,this);
where FileUtil is the following class
public final class FileUtil {
private static final String PRIMARY_VOLUME_NAME = "primary";
#Nullable
public static String getFullPathFromTreeUri(#Nullable final Uri treeUri, Context con) {
if (treeUri == null) return null;
String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri),con);
if (volumePath == null) return File.separator;
if (volumePath.endsWith(File.separator))
volumePath = volumePath.substring(0, volumePath.length() - 1);
String documentPath = getDocumentPathFromTreeUri(treeUri);
if (documentPath.endsWith(File.separator))
documentPath = documentPath.substring(0, documentPath.length() - 1);
if (documentPath.length() > 0) {
if (documentPath.startsWith(File.separator))
return volumePath + documentPath;
else
return volumePath + File.separator + documentPath;
}
else return volumePath;
}
private static String getVolumePath(final String volumeId, Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
return null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
return getVolumePathForAndroid11AndAbove(volumeId, context);
else
return getVolumePathBeforeAndroid11(volumeId, context);
}
private static String getVolumePathBeforeAndroid11(final String volumeId, Context context){
try {
StorageManager mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
Method getUuid = storageVolumeClazz.getMethod("getUuid");
Method getPath = storageVolumeClazz.getMethod("getPath");
Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
Object result = getVolumeList.invoke(mStorageManager);
final int length = Array.getLength(result);
for (int i = 0; i < length; i++) {
Object storageVolumeElement = Array.get(result, i);
String uuid = (String) getUuid.invoke(storageVolumeElement);
Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) // primary volume?
return (String) getPath.invoke(storageVolumeElement);
if (uuid != null && uuid.equals(volumeId)) // other volumes?
return (String) getPath.invoke(storageVolumeElement);
}
// not found.
return null;
} catch (Exception ex) {
return null;
}
}
#TargetApi(Build.VERSION_CODES.R)
private static String getVolumePathForAndroid11AndAbove(final String volumeId, Context context) {
try {
StorageManager mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
List<StorageVolume> storageVolumes = mStorageManager.getStorageVolumes();
for (StorageVolume storageVolume : storageVolumes) {
// primary volume?
if (storageVolume.isPrimary() && PRIMARY_VOLUME_NAME.equals(volumeId))
return storageVolume.getDirectory().getPath();
// other volumes?
String uuid = storageVolume.getUuid();
if (uuid != null && uuid.equals(volumeId))
return storageVolume.getDirectory().getPath();
}
// not found.
return null;
} catch (Exception ex) {
return null;
}
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static String getVolumeIdFromTreeUri(final Uri treeUri) {
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
final String[] split = docId.split(":");
if (split.length > 0) return split[0];
else return null;
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static String getDocumentPathFromTreeUri(final Uri treeUri) {
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
final String[] split = docId.split(":");
if ((split.length >= 2) && (split[1] != null)) return split[1];
else return File.separator;
}
}
UPDATE:
To address the Downloads issue mentioned in the comments: If you select Downloads from the left drawer in the default Android file picker you are not actually selecting a directory. Downloads is a provider. A normal folder tree uri looks something like this:
content://com.android.externalstorage.documents/tree/primary%3ADCIM
The tree uri of Downloads is
content://com.android.providers.downloads.documents/tree/downloads
You can see that the one says externalstorage while the other one says providers. That is why it cannot be matched to a directory in the file system. Because it is not a directory.
SOLUTION: You can add an equality check and if the tree uri is equal to that then return the default download folder path which can be retrieved like this:
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
And do something similar for all the providers if you wish to. And it would work correctly most of the time I assume. But I imagine that there are edge cases where it wouldn't.
thanx to #DuhVir for supporting the Android R case
In my activity I want to capture the actual path, which seems to be impossible.
That's is because there may not be an actual path, let alone one you can access. There are many possible document providers, few of which will have all their documents locally on the device, and few of those that do will have the files on external storage, where you can work with them.
I have quite a bit of data that will be downloaded and stored and I want the user to be able to place it where they want
Then use the Storage Access Framework APIs, rather than thinking that documents/trees that you get from the Storage Access Framework are always local. Or, do not use ACTION_OPEN_DOCUMENT_TREE.
In 5.0 I want to enable the user to get read/write permissions to that folder
That is handled by the storage provider, as part of how the user interacts with that storage provider. You are not involved.
It's addition to #Anonymous answer for Android 11.
#TargetApi(Build.VERSION_CODES.R)
private static String getVolumePath_SDK30(final String volumeId, Context context) {
try {
StorageManager mStorageManager =
(StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
if (mStorageManager == null) return null;
List<StorageVolume> storageVolumes = mStorageManager.getStorageVolumes();
for (StorageVolume storageVolume : storageVolumes) {
String uuid = storageVolume.getUuid();
Boolean primary = storageVolume.isPrimary();
if (primary == null) return null;
// primary volume?
if (primary && PRIMARY_VOLUME_NAME.equals(volumeId))
return storageVolume.getDirectory().getPath();
// other volumes?
if (uuid != null && uuid.equals(volumeId))
return storageVolume.getDirectory().getPath();
}
// not found.
return null;
} catch (Exception ex) {
return null;
}
}
I was trying to add a default save directory before or if user does not select a custom directory using SAF UI in preferences screen of my app. It's possible for users to miss selecting a folder and app may crash. To add a default folder in device memory you should
DocumentFile saveDir = null;
saveDir = DocumentFile.fromFile(Environment.getExternalStorageDirectory());
String uriString = saveDir.getUri().toString();
List<UriPermission> perms = getContentResolver().getPersistedUriPermissions();
for (UriPermission p : perms) {
if (p.getUri().toString().equals(uriString) && p.isWritePermission()) {
canWrite = true;
break;
}
}
// Permitted to create a direct child of parent directory
DocumentFile newDir = null;
if (canWrite) {
newDir = saveDir.createDirectory("MyFolder");
}
if (newDir != null && newDir.exists()) {
return newDir;
}
This snippet will create a directory inside main memory of device and grant read/write permissions for that folder and sub-folders. You can't directly create MyFolder/MySubFolder hierarchy, you should create another directory again.
You can check if that directory has permission to write, as far i seen on 3 devices, it returns true if it's created using DocumentFileinstead of File class. This is a simple method for creating and granting write permission for Android >= 5.0 without using ACTION_OPEN_DOCUMENT_TREE
public static String findFullPath(String path) {
String actualResult="";
path=path.substring(5);
int index=0;
StringBuilder result = new StringBuilder("/storage");
for (int i = 0; i < path.length(); i++) {
if (path.charAt(i) != ':') {
result.append(path.charAt(i));
} else {
index = ++i;
result.append('/');
break;
}
}
for (int i = index; i < path.length(); i++) {
result.append(path.charAt(i));
}
if (result.substring(9, 16).equalsIgnoreCase("primary")) {
actualResult = result.substring(0, 8) + "/emulated/0/" + result.substring(17);
} else {
actualResult = result.toString();
}
return actualResult;
}
this function gives me the absolute path from tree uri.
this solution working on most of device i tested in more than 1000 devices.
it also gives us the right absolute path of folder which contained in memory card or OTG.
How it Works?
basically most of devices have the path that starts with /Storage/ prefix. and the middle part of path contains mounted point name i.e /emulated/0/ for internal Storage, or some string like /C0V54440/ etc (just example). and the last segment is path from root of storage to folder like /movie/piratesofthecarribian
so, the path we constructed from :- /tree/primary:movie/piratesofthecarribian is :- /storage/emulated/0/movie/piratesofthecarribian
You can find more information on my github repo. visit there to get the android code about how to convert tree uri to actual absolute path.
gihub(https://github.com/chetanborase/TreeUritoAbsolutePath)
public static String findFullPath(String path) {
String actualResult="";
path=path.substring(5);
int index=0;
StringBuilder result = new StringBuilder("/storage");
for (int i = 0; i < path.length(); i++) {
if (path.charAt(i) != ':') {
result.append(path.charAt(i));
} else {
index = ++i;
result.append('/');
break;
}
}
for (int i = index; i < path.length(); i++) {
result.append(path.charAt(i));
}
if (result.substring(9, 16).equalsIgnoreCase("primary")) {
actualResult = result.substring(0, 8) + "/emulated/0/" + result.substring(17);
} else {
actualResult = result.toString();
}
return actualResult;
}
I have the following code that correctly attaches the image to the email and sends:
Intent sharingIntent = new Intent(Intent.ACTION_SEND);
sharingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// Set tht type to image/* and add the extra text field for the message to send
sharingIntent.setType(Application2.instance().getResString(R.string.share_intent_type_text_image));
sharingIntent.putExtra(Intent.EXTRA_TEXT, String.format(Application2.instance().getResString(R.string.share_intent_body_question), question.question));
if (destFile != null)
{
Uri uri = Uri.fromFile(destFile);
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
((ActivityMain) getActivity()).startActivity(Intent.createChooser(sharingIntent, "Share via"));
}
R.string.share_intent_type_text_image is defined as "image/png"
destFile is an image grabbed from the external cache directory of the app, (((ActivityMain) getActivity()).getExternalCacheDir()
However, when I attempt to open the file in Gmail, a dialog appears that says: Info - No app can open this attachment for viewing. I've downloaded the file via my PC and the extension comes up as .File. I can open it with paint and other image viewers.
Anyone experience this before?
Considering the FileProvider problems, and also because I wanted to implement a max cache size for collected temp files, I went with a ContentProvider solution and it works a treat. Basically, you're allowed to use your internal cache without any problem but still provide third party apps with a URI they can use to reference your temporary files you want to share with them. Because you use your internal cache, there will be no unnecessary WRITE_EXTERNAL_STORAGE permission to ask for.
The added max cache size limit (that you can remove from the class by simply deleting everything from checkSize() to the end of the class, for instance, if you can make sure you delete all files directly after sharing, so they won't remain on the device) works by checking the cumulated max size upon each call and clearing up half the cache (deleting the oldest files) if necessary.
public class TemporaryFile extends ContentProvider {
private static final long MAX_SIZE = 512 * 1024;
// commented out on purpose so that you don't forget to rewrite it...
// public static final String AUTHORITY = "com.example.tempfile";
private UriMatcher uriMatcher;
#Override
public boolean onCreate() {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "*", 1);
return true;
}
#Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
if (uriMatcher.match(uri) == 1) {
final String file = getContext().getCacheDir() + File.separator + uri.getLastPathSegment();
return ParcelFileDescriptor.open(new File(file), ParcelFileDescriptor.MODE_READ_ONLY);
}
else
throw new FileNotFoundException(uri.toString());
}
#Override
public int update (Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
#Override
public int delete (Uri uri, String selection, String[] selectionArgs) {
return 0;
}
#Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
#Override
public String getType(Uri uri) {
return null;
}
#Override
public Cursor query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
public static File getFile(Context context, String prefix, String extension) throws IOException {
checkSize(context);
File file = File.createTempFile(prefix, extension, context.getCacheDir());
file.setReadable(true);
file.deleteOnExit();
return file;
}
public static Uri getPublicUri(File file) {
return Uri.withAppendedPath(Uri.parse("content://" + AUTHORITY), file.getName());
}
public static void checkSize(Context context) throws IOException {
File dir = context.getCacheDir();
if (getDirSize(dir) > MAX_SIZE)
cleanDir(dir, MAX_SIZE / 2);
}
private static long getDirSize(File dir) {
long size = 0;
for (File file : dir.listFiles())
if (file.isFile())
size += file.length();
return size;
}
private static void cleanDir(File dir, long atLeast) {
long deleted = 0;
File[] files = dir.listFiles();
Arrays.sort(files, new Comparator<File>() {
public int compare(File f1, File f2) {
return Long.valueOf(f1.lastModified()).compareTo(f2.lastModified());
}
});
for (File file : files) {
deleted += file.length();
file.delete();
if (deleted >= atLeast)
break;
}
}
}
Using it couldn't be simpler, just call
File file = TemporaryFile.getFile(this, "prefix", ".extension");
whenever you want to create a new file and
TemporaryFile.getPublicUri(file)
whenever you want to get a public Uri to the file, eg. to pass it to an intent as data or Intent.EXTRA_STREAM.
Being a provider, don't forget to add the necessary manifest entry, either:
<provider
android:name=".TemporaryFile"
android:authorities="com.example.tempfile"
android:exported="true"
tools:ignore="ExportedContentProvider" >
</provider>
This works but requires external storage and the relating permissions. When downloading an app, a dialog will show that the app is requesting to be able to read/write data which may turn users away. Use the FileProvider as Simon suggested in my initial post if that's a concern.
Useful links:
https://developer.android.com/reference/android/support/v4/content/FileProvider.html
I attempted to use the File Provider as Simon suggested in my initial post to no avail. I received a NullPointerException on the following line:
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
I was unable to track the problem after following the guide at:
https://developer.android.com/reference/android/support/v4/content/FileProvider.html
as well as the other thread at:
How to use support FileProvider for sharing content to other apps?
At this point I realized there is no file type set for the images being used. I simply added .png to the files and the attachments work correctly in Gmail as well as the previous apps that already worked.
I provided the following code if anyone was curious how I shared an internal file. It's not complete and does not handle errors completely but it may be useful for someone as a start.
// Copy image file to external memory and send with the intent
File srcFile = getImage();
File destDir = new File(((ActivityMain) getActivity()).getExternalCacheDir(),
Application2.instance().getResString(R.string.temporary_external_image_path));
if(!destDir.exists())
{
destDir.mkdirs();
}
if(destDir != null && srcFile != null)
{
File destFile = new File(destDir, srcFile.getName());
if (!destFile.exists())
{
try
{
Application2.instance().copy(srcFile, destFile);
}
catch (IOException e)
{
if (BuildConfig.DEBUG) Log.e("Failed to copy file '" + srcFile.getName() + "'");
}
}
if (destFile != null)
{
Uri uri = Uri.fromFile(destFile);
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
((ActivityMain) getActivity()).startActivity(Intent.createChooser(sharingIntent, "Share via"));
}
}