Get real path from Uri - DATA is deprecated in android Q - android

I'm successfully implementing a method for retrieving the real path of an image from gallery by the Uri returned from ACTION_PICK intent. Here's a sample:
// getRealPathFromURI(intent.getData());
private String getRealPathFromURI(Uri contentURI) {
String result;
Cursor cursor = getContentResolver().query(contentURI, null, null, null, null);
if (cursor == null) { // Source is Dropbox or other similar local file path
result = contentURI.getPath();
} else {
cursor.moveToFirst();
int idx = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
result = cursor.getString(idx);
cursor.close();
}
return result;
}
Just like this answer.
Recently updated the compileSdkVersion to 29 and apparently the DATA attribute everyone's using is deprecated.
In the official docs, they recommend to use FileDescriptor instead, problem is i don't know exactly how.
Only thing i found is this question. Didn't find a proper answer there though.
Please help me overcome that deprecation issue with a solution using the suggested way or any other way.
Thank you.
Update:
Followed #CommonsWare's answer and copied the returned Uri (of an image the user picked) to a local directory, using context.getContentResolver.openInputStream(Uri).
Even tried retrieving a file from Google Drive - and it worked. Only problem was the long time it took (about 20 sec for 5MB file).
As a bonus, i was cleared to remove external storage permissions, which one doesn't need for using app's local directories.
No more externals paths for me!

This question came up for me too a week ago.
My solution was to create an InputStream from the URI and then, from this, create an OutputStream by copying the contents of the input stream.
Note: You could call this method using an asynchronous call because copying extremely large files could have some delays and you won't want to block your UI
#Nullable
public static String createCopyAndReturnRealPath(
#NonNull Context context, #NonNull Uri uri) {
final ContentResolver contentResolver = context.getContentResolver();
if (contentResolver == null)
return null;
// Create file path inside app's data dir
String filePath = context.getApplicationInfo().dataDir + File.separator
+ System.currentTimeMillis();
File file = new File(filePath);
try {
InputStream inputStream = contentResolver.openInputStream(uri);
if (inputStream == null)
return null;
OutputStream outputStream = new FileOutputStream(file);
byte[] buf = new byte[1024];
int len;
while ((len = inputStream.read(buf)) > 0)
outputStream.write(buf, 0, len);
outputStream.close();
inputStream.close();
} catch (IOException ignore) {
return null;
}
return file.getAbsolutePath();
}

I'm successfully implementing a method for retrieving the real path of an image from gallery by the Uri returned from ACTION_PICK intent.
That code may not work for all images. There is no requirement for DATA to point to a filesystem path that you can access.
Just like this answer.
FWIW, this was my answer to that question.
Only thing i found is this question. Didn't find a proper answer there though.
That technique wasn't particularly good and will no longer work, as Android has locked down /proc.
In the official docs, they recommend to use FileDescriptor instead, problem is i don't know exactly how.
The more general concept is that you use ContentResolver to work with the Uri, whether you get an InputStream (openInputStream()), OutputStream (openOutputStream()), or FileDescriptor. Consume the content using those things. If you have some API that absolutely needs a file, copy the content (e.g., from the InputStream) to a file that you control (e.g., in getCacheDir()).
As a bonus, now your code is also in position to use the Storage Access Framework (e.g., ACTION_OPEN_DOCUMENT) and the Internet (e.g., OkHttp), if and when that would be useful.

Related

Does passing the Activity Context to an singleton affect the getContentResolver()?

**Problem:**I'm updating an app to Android 11 (API 30) and I cannot get the getContentResolver() to work in the following instance though it works elsewhere.
With help from this forum and reading the Scoped Storage docs, elsewhere in the app, the following code works within the Actiities they reside; which includes inputting and outputting from the shared download folder using the getContentResolver(). The file types are a variety.
What I'm trying to do: This app also allows a user (with approved permissions) to attach a variety of files (e.g., docs, audio, video, etc.) to notes in a database. In this case, a PDF. However, the file information gets passed to a Singleton class created to avoid redundancy in the same code within other activities, so I just pass the Context if needed.
As I mentioned, this code works fine when contained within the Activity and the only difference I see is the context is passed to a singleton class. I was hoping someone would see something I'm not.
I do realize the URI is not passed (see the resultLauncher code, but I don't believe that really matters, I can take the file path and change it to an URI, which I have successfully multiple times. Aside from passing to a singleton class, this is the only difference (I can see) compared to my other code.
I could be way off here, the first app and all. A user needs to have the option to attach multiple file types.
**INTENT**
btnAddFile.setOnClickListener(v -> {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
resultLauncher.launch(intent);
});
RESULT LAUNCHER
resultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null){
Uri uri = result.getData().getData();
String util = UriUtils.getPathFromUri(this, uri);
tableLayoutFiles.addView(BuildTableLayout.setupFilesTableRow(EditNote.this,tableLayoutFiles, util,false));
}
});
CODE WITHIN SINGLETON CLASS METHOD (a TableLayout of files and file paths TextViews are passed. So the following code resides in a loop iterating the TextView (i.e., tv) file paths.
File f = new File(tv.getText().toString());
String n = f.getName();
try {
InputStream fis = context.getContentResolver().openInputStream(Uri.fromFile(new File(f.getPath())));
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
for (int read; (read = fis.read(buf)) != -1; ) {
bos.write(buf, 0, read);
}
fis.close();
noteFiles.add(new Files(0, n, bos.toByteArray()));
} catch (Exception e) {
Log.e("getContentResolver", e.toString());
e.printStackTrace();
}
Well I solved my own problem and as I suspected, I had to pass the Uri directly to the getContentResolver().openInputStream() and could not convert a path using Uri.fromFile(). Another learners experience.
Adding a new variable HashMap<String, Uri>() called fileURIs to my method, I passed the file Uri to my method. I used a file path as the key and the file's Uri as the key value. I updated the getContentResolver().openInputStream() within the for loop to retrieve the correct file Uri.
Update:
InputStream fis = context.getContentResolver().openInputStream(fileURIs.get(f.getPath()));
File f = new File(tv.getText().toString());
String n = f.getName();
try {
InputStream fis = context.getContentResolver().openInputStream(fileURIs.get(f.getPath()));
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
for (int read; (read = fis.read(buf)) != -1; ) {
bos.write(buf, 0, read);
}
fis.close();
noteFiles.add(new Files(0, n, bos.toByteArray()));
} catch (Exception e) {
Log.e("getContentResolver", e.toString());
e.printStackTrace();
}
And I should clarify, the use of Uri.fromFile() does work on file/directories that are not "access denied", so passing a cache file path can be passed to the ContextResolver() without issue in this fashion. In my case, when files are imported from an xml, they get moved to the cache directory which eliminates the issue and why Uri.fromFile() works. That was part of my confusion and not being fully eductated on scoped storage.

Android - Access External Storage Files Android 10/11

I really need your help because I'm stuck <.<
I have already tryed all solutions I've found here in "stackoverflow" for this problem, but any of them worked for me..
I have updated my application to follow the new Google Play Store policies, so actually my app is using only "Scoped Storage" without "Shared Storage" behaviour, so I've removed from the manifest the "requestLegacyExternalStorage".
In my app I need to send to the server some file selected by the user, these file can be stored in the "internal storage" or in the "external storage". They are always stored outside of the app-specific directory, so the directory in:
"/storage/emulated/0/Android/data/[APP_PACKAGE]/files"
My biggest problem is that I can't open any file stored outside of the app-specific directory!
I really need to open the files that the user has selected and convert its content to "base64" to send it to the server.
But when I use this method to get the file content encoded in base64:
public static String fileToBase64(#NonNull String filePath) {
String ret = null;
byte[] buffer = new byte[8192];
int bytesRead;
try(ByteArrayOutputStream bAOS = new ByteArrayOutputStream();
Base64OutputStream b64OS = new Base64OutputStream(bAOS, Base64.DEFAULT);
InputStream iS = new FileInputStream(filePath)){
while ((bytesRead = iS.read(buffer)) != -0x1) {
b64OS.write(buffer, 0x0, bytesRead);
}
b64OS.flush();
ret = bAOS.toString();
} catch (IOException ioE) {
Logger.onException(TAG, ioE);
}
return ret;
}
I always get an ACCESS PERMISSION DENIED.
Is there any way to solve this problem without using "requestLegacyStorage" in the manifest?
I know that Google will remove all applications that don't use only "SCOPED STORAGE" from the store starting from 5 of july, so I can't use the "reqestLegacyStorage" to solve this problem...
I'm already requesting and giving the "READ_EXTERNAL_STORAGE" and "WRITE_EXTERNAL_STORAGE" permissions, but I still can't open the file content if it is stored outside of the dedicated application directory...
I can only open and encode file content if it is in the app directory (so: "/storage/emulated/0/Android/data/[APP_PACKAGE]/files"), but I need to upload to my server files choosed by the user so these files were never stored inside the app directory, they are always stored inside the internal storage or the external storage in directories like the "Download" dir or the "Pictures" dir (I need to upload every type of files, so pictures, documents, pdfs ecc.. ecc.)
Is there any solution to this problem?
I have already tryed all the solutions I found online, but I always get an ACCESS EXCEPTION if I don't add "requestLegacyStorage" to the manifest (and I can't add it to notg go against Google's policies...)
Please I really need to solve this problem because this is one of the most important feature of my app..
Thank you so much!
I hope anybody can help me solve this problem T_T
Have a nice day and nice coding!
(Ask if you need more informations and I will add them!)
##########################################################################
If anyone needs it I found a "solution" but works only by using "ACTION_GET_CONTENT" (and probably by using "ACTION_OPEN_DOCUMENT", but I didn't try it yet).
When you select a file (stored outside the app-specific directory) using "ACTION_GET_CONTENT" this file is copied inside the app-specific directory ("/storage/emulated/0/Android/data/[APP_PACKAGE]/files") so you can open it because it agrees with the "SCOPED STORAGE" policy.
# "ACTION_GET_CONTENT" code:
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
String[] mimes = {
"application/*",
"audio/*",
"font/*",
"image/*",
"message/*",
"model/*",
"multipart/*",
"text/*",
"video/*"
};
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimes);
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
startActivityForResult(Intent.createChooser(intent, getString(R.string.msg_select_file_to_upload)), REQ_CODE_OPEN_DOCUMENT);
Theoretically it also works without passing the "mimes" array to the intent extra "EXTRA_MIME_TYPES".
To get the path inside the "onActivityResult":
String path = FilesUtils.onActivityResultOpenDocument(this, data);
public static String onActivityResultOpenDocument(Context context, Intent data){
String selectedPath, fileName;
Uri uri = data.getData();
String mimeType = context.getContentResolver().getType(uri);
if (mimeType == null) {
String path = getPathFromOpenDocumentUri(context, uri);
if (path == null) {
fileName = FilenameUtils.getName(uri.toString());
} else {
File file = new File(path);
fileName = file.getName();
}
} else {
Uri returnUri = data.getData();
Cursor returnCursor = context.getContentResolver().query(returnUri, null, null, null, null);
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);
returnCursor.moveToFirst();
fileName = returnCursor.getString(nameIndex);
}
String sourcePath = context.getExternalFilesDir(null).toString();
selectedPath = formatFilepath(sourcePath, fileName);
File fileSave = new File(selectedPath);
try {
copyUriStreamToFile(context, uri, fileSave);
} catch (Exception e) {
Logger.onException(TAG, e);
Toast.makeText(context, R.string.error_impossibile_recuperare_file, Toast.LENGTH_SHORT).show();
selectedPath = null;
}
return selectedPath;
}
So summarizing by using "ACTION_GET_CONTENT" (and maybe "ACTION_OPEN_DOCUMENT" too, but I didn't tryed this) the selected file is copied inside the app-specific directory (so: "/storage/emulated/0/Android/data/[APP_PACKAGE]/files") in this way the file can be opened because it agrees with the "Scoped Storage" policy.
Thank you all for your answers and your time! (: <3
I still don't know how to read a file if it is stored outside the app-specific directory, so if anybody know it please share your solution (:

Is there a way to get 100% correct file extension in Android with scoped storage enabled?

I'm trying to implement a file sending functionality in my Android app (any files are allowed, and the files don't belong to my app). From the ACTION_OPEN_DOCUMENT I receive an InputStream, then I make a temp File object with the name I'm getting from ContentResolver's OpenableColumns.DISPLAY_NAME, and then send the file. The reason I do all of this is that I work with a 3rd party API which allows for File objects only.
But the OpenableColumns.DISPLAY_NAME doesn't guarantee that I get the file name with a file extension as stated in the docs. As far as I understand, there is no way to get the actual filename or physical path of a file with the Scoped Storage enforced in the newer versions of Android. Therefore, I have to check if a filename contains an extension, and if not - get the file's MIME type with ContentResolver and the most common extension for it using the MimeTypeMap. This approach feels to be not very reliable since I have to rely on both ContentResolver correctly determining the MIME type and MimeTypeMap retrieving the correct extension. Getting the extension is crucial at least because users should be able to download and open files on their PC from a desktop app.
So, is it possible to get a filename or at least file extension with a 100% guarantee with scoped storage enabled? Or maybe is there a more efficient way to handle my situation? I'd appreciate some help with this.
Try this method, it helped me:
public static String getFileName(Uri uri, Context context) {
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;
}

How to get Path of image which is shared from whatsapp in android [duplicate]

I'm successfully implementing a method for retrieving the real path of an image from gallery by the Uri returned from ACTION_PICK intent. Here's a sample:
// getRealPathFromURI(intent.getData());
private String getRealPathFromURI(Uri contentURI) {
String result;
Cursor cursor = getContentResolver().query(contentURI, null, null, null, null);
if (cursor == null) { // Source is Dropbox or other similar local file path
result = contentURI.getPath();
} else {
cursor.moveToFirst();
int idx = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
result = cursor.getString(idx);
cursor.close();
}
return result;
}
Just like this answer.
Recently updated the compileSdkVersion to 29 and apparently the DATA attribute everyone's using is deprecated.
In the official docs, they recommend to use FileDescriptor instead, problem is i don't know exactly how.
Only thing i found is this question. Didn't find a proper answer there though.
Please help me overcome that deprecation issue with a solution using the suggested way or any other way.
Thank you.
Update:
Followed #CommonsWare's answer and copied the returned Uri (of an image the user picked) to a local directory, using context.getContentResolver.openInputStream(Uri).
Even tried retrieving a file from Google Drive - and it worked. Only problem was the long time it took (about 20 sec for 5MB file).
As a bonus, i was cleared to remove external storage permissions, which one doesn't need for using app's local directories.
No more externals paths for me!
This question came up for me too a week ago.
My solution was to create an InputStream from the URI and then, from this, create an OutputStream by copying the contents of the input stream.
Note: You could call this method using an asynchronous call because copying extremely large files could have some delays and you won't want to block your UI
#Nullable
public static String createCopyAndReturnRealPath(
#NonNull Context context, #NonNull Uri uri) {
final ContentResolver contentResolver = context.getContentResolver();
if (contentResolver == null)
return null;
// Create file path inside app's data dir
String filePath = context.getApplicationInfo().dataDir + File.separator
+ System.currentTimeMillis();
File file = new File(filePath);
try {
InputStream inputStream = contentResolver.openInputStream(uri);
if (inputStream == null)
return null;
OutputStream outputStream = new FileOutputStream(file);
byte[] buf = new byte[1024];
int len;
while ((len = inputStream.read(buf)) > 0)
outputStream.write(buf, 0, len);
outputStream.close();
inputStream.close();
} catch (IOException ignore) {
return null;
}
return file.getAbsolutePath();
}
I'm successfully implementing a method for retrieving the real path of an image from gallery by the Uri returned from ACTION_PICK intent.
That code may not work for all images. There is no requirement for DATA to point to a filesystem path that you can access.
Just like this answer.
FWIW, this was my answer to that question.
Only thing i found is this question. Didn't find a proper answer there though.
That technique wasn't particularly good and will no longer work, as Android has locked down /proc.
In the official docs, they recommend to use FileDescriptor instead, problem is i don't know exactly how.
The more general concept is that you use ContentResolver to work with the Uri, whether you get an InputStream (openInputStream()), OutputStream (openOutputStream()), or FileDescriptor. Consume the content using those things. If you have some API that absolutely needs a file, copy the content (e.g., from the InputStream) to a file that you control (e.g., in getCacheDir()).
As a bonus, now your code is also in position to use the Storage Access Framework (e.g., ACTION_OPEN_DOCUMENT) and the Internet (e.g., OkHttp), if and when that would be useful.

Fetching attachments with custom extension in my Android application

What I am trying to achieve is sounds very familiar, it has been posted many times here and there in Stack Overflow as well, but I'm unable to get it done.
The scenario is, I receive a mail with attachment having custom extension in it. The extension is recognized by my app and it needs the FilePath to process it.
Currently, when I get the attachment in my app using getIntent().getData() all I get is path of the form content://
I have seen methods to convert media content of the type content:// to FilePath like /sdcard/file.ext but I was unable to convert the attachment using that. May be its obvious.
Is there any way that I can process the content:// type without actually downloading it.
Currently from the k9 mail app, when I get the custom extension, it shows my app in the list and opens it through it, but I need FilePath like /sdcard/file.ext and I'm only able to get content:// type.
I hope I made the question clear.
Please Help.
Regards.
A content:// Uri does not necessarily point to a file on the sdcard.
It is more likely that it points to any kind of data stored in a database
or to a content provider that gives you access to the private file storage of another app.
I think the later one is the case with mail attachments (if the content provider is not requesting it directly from a web server). So converting the content:// Uri to a path will not work.
I did the following (not sure if it works also for k9 mail app)
Uri uri = intent.getData();
if (uri.getScheme().equals("content")) {
String fileName = ContentProviderUtils.getAttachmentName(this, uri);
if (fileName.toLowerCase().endsWith(".ext")) {
InputStream is = this.getContentResolver().openInputStream(uri);
// do something
} else {
// not correct extension
return;
}
} else if (uri.getScheme().equals("file")) {
String path = uri.getPath();
if (path.toLowerCase().endsWith(".ext")) {
InputStream is = new FileInputStream(path);
// do something
} else {
// not correct extension
return;
}
}
The attachment name can be found by
public static String getAttachmentName(Context ctxt, Uri contentUri) {
Cursor cursor = ctxt.getContentResolver().query(contentUri, new String[]{MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null);
String res = "";
if (cursor != null){
cursor.moveToFirst();
int nameIdx = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
res = cursor.getString(nameIdx);
cursor.close();
}
return res;
}

Categories

Resources