Android Q RecoverableSecurityException not granting access - android

I'm trying to get my file handling sorted for Android API 29. Since I'm no star in anything related to files on Android, I've run into a problem.
When I add images to my app I can delete them using contentResolver.delete(deleteUri, null, null); and everything works fine. But when the app is removed and then reinstalled it gives me a RecoverableSecurityException, this is accounted for and I've made it so that permission is requested to the file to be able to delete it. When permission is granted and I'm trying to delete the file again, it still gives me
android.app.RecoverableSecurityException: **app**.debug has no access to content://media/external/images/media/280. It is removed from the ContentResolver since it's no longer visible in any of the galleries and it returns no result when queried for, but the file is still "physically" on the device.
Where do I need to look to fix this problem? There is only one result from the ContentResolver, the file path it shows in a different error(shown together with the above error) is correct: E/MediaProvider: Couldn't delete /storage/emulated/0/Pictures/**app**/**filename**.jpeg
Delete file function:
Cursor c = getCursorFromContentResolver(fileName);
if (c != null && c.moveToFirst())
{
long id = c.getLong(c.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
Uri deleteUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
try
{
contentResolver.delete(deleteUri, null, null);
}catch (RecoverableSecurityException e)
{
//After the below Intent returns, the current function is run again
activity.startIntentSenderForResult(e.getUserAction().getActionIntent().getIntentSender(), requestCode, null, 0, 0, 0, null);
}
c.close();
return true;
}
Other permissions are requested:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

I have come across a very similar issue in Android Q, when deleting an image using Content resolver delete call. It turned out that despite me catching RecoverableSecurityException for permission to delete the image from the android gallery - it still has thrown an error that it could not delete the file because the app doesn't have permission (thus after opening Google Photos it would scan for images and find the "undeleted" one making it come back). This is where I saw the same error as in your question. When I tried the same code on Android R file did not come back.
After reading on this issue I tried SAF as a solution. The way I managed to delete image for good was to get the user to select a directory of the images (in my case DCIM/Camera) using Intent.ACTION_OPEN_DOCUMENT_TREE
Then I've captured Uri of the parent folder and name of the file in that folder when delete button was pressed:
String filename = queryUriName(content, photoUri);
DocumentFile docF = DocumentFile.fromTreeUri(context,myTree);
Boolean docex = docF.exists();
String idDoc = DocumentsContract.getTreeDocumentId(myTree);
idDoc = idDoc + "/"+filename;
Uri childrenUri = DocumentsContract.buildDocumentUriUsingTree(myTree,idDoc);
DocumentFile childfile = DocumentFile.fromSingleUri(context,childrenUri);
Boolean chex = childfile.exists();
System.out.println("child exist: "+ chex+ " file name is " + filename +" "+idDoc);
This allowed to delete the actual file and from what I can tell it does not throw any errors and file is gone.
Take this with caution as I am ~3 weeks into android dev and could not find problem described nor solution elsewhere. But would be happy to expand on or update the answer if needed.

In case, if someone looking to handle RecoverableSecurityExceptionin your catch block from android 10 onwards
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if(e instanceof RecoverableSecurityException){
RecoverableSecurityException recoverableSecurityException = (RecoverableSecurityException) e;
IntentSender intentSender = recoverableSecurityException.getUserAction().getActionIntent().getIntentSender();
if(intentSender != null){
try {
startIntentSender(intentSender, null, 0, 0, 0);
} catch (IntentSender.SendIntentException sendIntentException) {
sendIntentException.printStackTrace();
}
}
}
}

Related

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 any way of writing files to SDCard with flutter File.writeAsStringSync

I am trying to write to a file that is located in the SDCard, I found out that I need special permission for removable storage something that is not found in any known permission handler plugin for flutter (i tried simple_permission and permission_handler with no use).
I tried to acquire those permissions using the android side of things, so I wrote a simple function that would show the dialog and the user would allow the app to modify the content of the SDCard.
even after acquiring the rights to the SDCARD, I still get the same permissions denied error when trying to save files to the SDCard when using File.writeAsStringSync method.
I want to know if there is any known way/hack/workaround to save files in SDCards in flutter.
The android code i used is the same from this answer : https://stackoverflow.com/a/55024683/6641693
NOTE : I am targetting android 7 and beyond but not android 11.
I solved This, by ditching the dart file saving and using the android SAF.
First, what I did was try to get the sdCard modification permissions.
After that, I get to save the files I need.
here is the code I used to get the permissions ( aka the "allow this app to modify content on your sdCard" dialog )
public void takeCardUriPermission(String sdCardRootPath) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
File sdCard = new File(sdCardRootPath);
StorageManager storageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
StorageVolume storageVolume = storageManager.getStorageVolume(sdCard);
Intent intent = storageVolume.createAccessIntent(null);
try {
startActivityForResult(intent, 4010);
} catch (ActivityNotFoundException e) {
Log.e("TUNE-IN ANDROID", "takeCardUriPermission: "+e);
}
}
}
protected void onActivityResult(int requestCode, int resultCode, #Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 4010) {
Uri uri = data.getData();
grantUriPermission(getPackageName(), uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_READ_URI_PERMISSION);
final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_READ_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(uri, takeFlags);
methodChannel.invokeMethod("resolveWithSDCardUri",getUri().toString());
}
}
public Uri getUri() {
List<UriPermission> persistedUriPermissions = getContentResolver().getPersistedUriPermissions();
if (persistedUriPermissions.size() > 0) {
UriPermission uriPermission = persistedUriPermissions.get(0);
return uriPermission.getUri();
}
return null;
}
So in order to start the whole permissions acquiring process, you have to first call takeCardUriPermission and passing the URI of the sdCard path.
Note: on my FlutterActivity, i am able to get the sdCardPath directly using getExternalCacheDirs()[1].toString()
After calling takeCardUriPermission and once the allow button is pressed (or the decline) an activity result event will be called and the onActivtyResult method will be called. the requestCode check is useful when you have multiple events and you need to filter this one out.
The activity result code will give the app permissions to modify the files on the sdCard.
The getUri method is the one that we will be using afterwards when trying to save bytes to a file, it returns the URI of the SDCard that we selected (you can have multiple sdCards).
Saving Files
What I used to save a file is a straightforward method. First we need to get the URI of the sdCard and create a Documentfile out of it, then we go through the hierarchy of that directory (DocumentFile can reference files and directories) to find the needed file based on it's URI.
We do this search by splitting the file URI into parts and then navigating the hierarchy by testing if each part exists or not. Once we test all the parts we would have reached our file, if it exists, or we were stuck at the last directory we got to.
the resulting of this iteration is a DocumentFile that we can execute operations on and with.
the following is the full file saving code :
String filepath = (String) arguments.get("filepath");
final byte[] bytes = methodCall.argument("bytes");
try{
if(filepath==null || bytes==null)throw new Exception("Arguments Not found");
DocumentFile documentFile = DocumentFile.fromTreeUri(getApplicationContext(), getUri());
String[] parts = filepath.split("/");
for (int i = 0; i < parts.length; i++) {
DocumentFile nextfile = documentFile.findFile(parts[i]);
if(nextfile!=null){
documentFile=nextfile;
}
}
if(documentFile!=null && documentFile.isFile()){
OutputStream out = getContentResolver().openOutputStream(documentFile.getUri());
out.write(bytes);
out.close();
}else{
throw new Exception("File Not Found");
}
}catch (Exception e){
result.error("400",e.getMessage(),e);
return;
}
result.success(true);
Note: in my code, I am calling this under the MethodChannel's MethodCallHandler which will give me the argument I need: filePath which is the String URI of the file I want to write to and the bytes byte array representing the data I want to save. The same can be said for the result.success
The file writing code is simple: open the file, write the data and close the file.

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

Andriod 10 How to check file exist

In Android Q, save pictures in app-specific directory,
path like = /data/user/0/xxx.xxx.xxx/files/phone/abc.jpg
not save in the external storage, use Device FileExplorer to view,
need to check if file exist, avoid to download again
,but in Android Q file.exist() not work
File newFile = new File(path);
newFile.exists();
always return false
this question. I need to use MediaStore or SAF to resolver it.
or other function to check it.
If I use MediaStore to check. use ContentResolver. May be like this:
public void getPhotoCursor(Uri uri) {
Cursor cursor = getContentResolver().query(uri, null, null, null, null, null);
try {
if (cursor != null && cursor.moveToFirst()) {
String displayName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
}
} finally {
cursor.close();
}
}
But I can't get the Uri form app-specific directory. If I get the Uri, how to use file descriptor to check.
or use SAF to check.
File testFile = new File(getExternalFilesDir()+"phone", "abc.jpg");
FileProvider.getUriForFile(,,testFile);
Intent testIntent = new Intent(Intent.ACTION_VIEW);
testIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
testIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
testIntent.setDataAndType();
startActivity(testIntent);
In the ActivityResult to check it
Any help will be apperciated
is my fault, every time open APP I will delete all the .jpg from APP-specific.
so into APP I want to check avoid download again. file exist always return false.

Move Image Taken With Camera Leaves Broken Link Unitl SD Card Remount

I have an application in which I can use the device's camera to take a picture. What I would like to do is to start the ACTION_IMAGE_CAPTURE intent without assigning an EXTRA_OUTPUT, and then move the file that is created in the default location to my own custom location using file.renameTo. My code is something like this:
/* Start camera activity without EXTRA_OUTPUT */
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(intent, _REQUESTCODE_ATTACH_CAMERA);
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
switch(requestCode) {
case _REQUESTCODE_ATTACH_CAMERA:
/* Get path to most recently added image */
final String[] imageColumns = { MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA };
final String imageOrderBy = MediaStore.Images.Media._ID + " DESC";
Cursor imageCursor = managedQuery(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, imageColumns, null, null, imageOrderBy);
String fullPath = "";
if(imageCursor.moveToFirst()){
fullPath = imageCursor.getString(imageCursor.getColumnIndex(MediaStore.Images.Media.DATA));
imageCursor.close();
}
File f = Environment.getExternalStorageDirectory();
f = new File(f.getAbsolutePath() + File.separator + "DCIM" + File.separator + MY_APP_NAME;
if(!f.exists()) {
f.mkdirs();
}
/* Create new file based on name of most recently created image */
File oldFile = new File(fullPath);
String newPath = f.getAbsolutePath() + File.separator + oldFile.getName() ;
/* Move file with renameTo */
oldFile.renameTo(new File(newPath));
break;
...
}
}
}
All of this works quite well, however there is one strange thing that is occurring. In my app, I have another button that allows selecting an existing image from the phone's gallery. That code looks like this:
Intent galleryIntent = new Intent(Intent.ACTION_GET_CONTENT);
galleryIntent.setType("image/*");
activity.startActivityForResult(galleryIntent, _REQUESTCODE_ATTACH_GALLERY);
This also works, but if I take a picture with the camera using the code posted above, and then try to select another image from the gallery, there will be blank "broken link" type items in the gallery that contain no content and are unselectable. These seem to correspond with photos taken and moved using renameTo; if I put in code in onActivityResult to post the filename to LogCat, the name that gets logged is the same as the name of the previously moved file that it corresponds to. Trying to create a File object or in any way access that filename, results in null objects and force closes.
The strange part is that there is no evidence of these "broken link" files in Eclipse DDMS, nor in the phone itself if I use Root Browser, and they disappear if I remount the SD Card.
The whole reason I am moving the images after capturing them with the camera is to avoid filling up the phone's gallery storage with unnecessary images. While these empty "broken link" type files don't appear to be taking up any storage space, they would still be very annoying to an end-user trying to browse through their gallery. Does anyone have any ideas on what is happening here or how to solve this problem?
EDIT:
Here is a photo showing what the gallery looks like with a "broken link" type image displayed. One of these will appear for every photo that is taken using my app, and they will all disappear if I remount the SD Card.
Thanks in part to this SO thread, I have discovered a solution. It actually makes sense that it would behave this way since there is a table kept for media content and so removing something without telling the table would definitely create a "broken link" type scenario.
The ultimate solution is to use contentResolver.delete to remove the reference to the file within the content resolver, but there are two different ways that I have found that will work.
/* Moving with renameTo */
//Use the same exact code as I had before (shortened for brevity) to move the file
oldFile.renameTo(newFile);
//Get URI from contentResolver using file Id from cursor
Uri oldUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, imageCursor.getString(imageCursor.getColumnIndex(MediaStore.Images.Media._ID)));
//Delete old file
getContentResolver().delete(oldUri, null, null);
Getting the URI in this way is necessary because it requires a reference to the image in the contentResolver rather than the path to its location in storage. This way might feel dirty to some since you are moving a file and then calling a delete function on that file in order to sort of trick the content resolver into removing the link to the file. If you would rather, you can do it without using renameTo so that the call to delete(...) actually does delete the image.
/* Moving with streams */
//Get streams
InputStream in = new FileInputStream(oldFile);
OutputStream out = new FileOutputStream(newFile);
byte[] buffer = new byte[1024];
int bytesRead = 0;
//Read old file into new file
while((bytesRead = in.read(buffer)) > 0) {
out.write(buffer, 0, bytesRead);
}
//Get URI from contentResolver using file Id from cursor
Uri oldUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, imageCursor.getString(imageCursor.getColumnIndex(MediaStore.Images.Media._ID)));
//Delete old file
getContentResolver().delete(oldUri, null, null);
The call to contentResolver.delete is the same either way, I just wanted to point out that it will still work if the image has already been removed.
During this I discovered a solution to a problem that I didn't even realize that I had that I will post here as well in case anyone with this same problem comes across this in the future. In order to keep the image as selectable in the device gallery from the new location, you need to let the media scanner know that a change has been made. There are two ways that I found to do this:
/* This is the only way that I know of to handle multiple new files at once. I
really would use this sparingly, however, since it will rescan the entire
SD Card. Not only could this take a long time if the user has a lot of files
on their card, it will also show a notification so it is not exactly a
transparent operation. */
sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://" + Environment.getExternalStorageDirectory())));
/* You *could* do multiple files with this by passing in the path for each one
in the array of Strings, however an instance of this will get called for each
one rather than it doing them all at once. Likewise, your onScanCompleted
(if you choose to include one) will get called once for each file in the list.
So really, while this is much better for a small number of files, if you plan
on scanning a very large amount then the full rescan above would probably be
a better option. */
MediaScannerConnection.scanFile(context, new String[]{ newFilePathAsString }, null,
new MediaScannerConnection.OnScanCompletedListener() {
public void onScanCompleted(String path, Uri uri) {
//This executes when scanning is completed
}
}
);

Categories

Resources