Handling content:// urls shared from Android DownloadManager - android

So, I want my app to be able to upload files that the user shares with it. I got it to handle files shared from my various file explorers through file:// urls, then handling things shared from, say, the Gallery via content:// URLs. All good, except then I randomly decided to test sharing from the DownloadManager.
Here's the code:
#Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if(intent.getAction() == Intent.ACTION_SEND) {
Uri fileUri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
if(fileUri != null) {
if(fileUri.getScheme().equals("content")) {
try {
Uri data = intent.getData();
InputStream input = getContentResolver().openInputStream(fileUri);
//Do something with InputStream
} catch(Exception e) {
//Log exception/show error
}
}
}
}
}
However, whenever I try to pass the Uri "data" into openInputStream I get a NullPointerException as getData() is returning null, even though other questions I've seen on this topic have said to use getData().
And when I try to pass "fileUri" into openInputStream, as I'm doing in the code I get the security exception:
06-20 12:41:24.310: E/AndroidRuntime(12375): java.lang.SecurityException: Permission Denial: reading com.android.providers.downloads.DownloadProvider uri content://downloads/all_downloads/433 from pid=12375, uid=10212 requires android.permission.ACCESS_ALL_DOWNLOADS, or grantUriPermission()
I've tried adding this permission but I'm aware that it isn't open for any app to use.
So, I have no idea how else to try and pull the file data out of the Intent, hopefully there's something obvious that I'm missing.

Related

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.

Storage Access Framework ACTION_CREATE_DOCUMENT on Google Drive results in 0 byte file sometimes

Suddendly my app's Google Drive backup/restore via Storage Access Framework feature stopped working properly. Users report it failing sometimes, and sometimes not, without a clear reason. Internal or external storage options instead work as expected.
This is my code:
private void createDocument(String fileName) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/json");
intent.putExtra(Intent.EXTRA_TITLE, fileName);
try {
startActivityForResult(intent, REQUEST_CREATE_DOCUMENT);
}
catch (ActivityNotFoundException ex) {
Toast.makeText(this, R.string.message_saf_not_supported, Toast.LENGTH_LONG).show();
}
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK && data != null) {
Uri uri = data.getData();
if (requestCode == REQUEST_CREATE_DOCUMENT && uri != null) {
ParcelFileDescriptor fileDescriptor = getApplicationContext().getContentResolver().openFileDescriptor(Uri.parse(uriString), "w");
if (fileDescriptor != null) {
FileOutputStream fileOutputStream = new FileOutputStream(fileDescriptor.getFileDescriptor());
// Write bytes...
fileOutputStream.close();
fileDescriptor.close();
}
}
}
super.onActivityResult(requestCode, resultCode, data);
}
I debugged my code and experienced this strange behavior.
If data to write is little and code is executed almost immediately it works most of the times.
If data is big (and usually is, around 10MB) or I put a breakpoint on my onActivityResult method openFileDescriptor or FileOutputStream creation throw a FileNotFoundException or even if that exception is not raised it fails silently and resulting file on Google Drive is 0 bytes long.
I even tried putting the ParcelFileDescriptor/FileOutputStream part in a background thread, AsyncTask, Worker without any luck. Problem is the same.
This is upsetting my users and making my ratings go way down. I'm sure I didn't anything on my code in the last two months, so the problem must be elsewhere, but I don't know...
EDIT: I might add that is seems somehow related to the Google Drive app, because if I clear its data or cache the subsequent uploads work fine, until it breaks again.

openInputStream cause SecurityException:Permission Denial (Android N+)

I need help with this problem since I'm new to Android.
My app support JellyBean (16) up to Oreo (26).
I have an UploadService that requires openInputStream() to upload data because the new behavior in Nougat.
This code works fine in Marshmallow and below, but always give me SecurityException crash on Nougat. And it crashes on the line where openInputStream() is called with error:
java.lang.SecurityException: Permission Denial: reading com.miui.gallery.provider.GalleryOpenProvider uri content://com.miui.gallery.open/raw/%2Fstorage%2Femulated%2F0%2FDCIM%2FCamera%2FIMG_20171008_182834.jpg from pid=30846, uid=10195 requires the provider be exported, or grantUriPermission()
The file uri could be from various app (gallery, camera, etc). I've narrowed down the problem to uri that comes from ACTION_GET_CONTENT intent (anything that comes from camera intent or MediaRecorder works fine).
I think it's because the uri lost its permission when passed into the service, but adding Intent.FLAG_GRANT_WRITE_URI_PERMISSION and Intent.FLAG_GRANT_READ_URI_PERMISSION doesn't help.
Also tried adding FLAG_GRANT_PERSISTABLE_URI_PERMISSION flag, but it still crashes and getContentResolver().takePersistableUriPermission() causes another SecurityException crash saying the said uri hasn't been granted persistable uri...
UploadService.java
//.......... code to prepare for upload
if ( contentResolver != null && schemeContentFile ) {
mMime = UtilMedia.getMime(this, uri);
try {
InputStream is = contentResolver.openInputStream(uri);
byte[] mBytes = getBytes(is);
Bundle fileDetail = UtilMedia.getFileDetailFromUri(this, uri);
Log.d("AndroidRuntime", TAG + " " + mMime + " " + UtilToString.bundleToString(fileDetail) + " imageFile " + mFile);
currTitle = fileDetail.getString(OpenableColumns.DISPLAY_NAME, "");
MediaType type = MediaType.parse(mMime);
requestFile = RequestBody.create(type, mBytes);
} catch ( IOException e ) {
e.printStackTrace();
} catch ( SecurityException e ) {
e.printStackTrace();
}
}
//............continue to upload
Thank You in advance.
EDIT (Additional Info)
In case this is important. The activity calling the service is calling finish() after it sends all the required data to the service, letting user to use the app, while the upload resumed in the background (with notification to tell user). And also, the upload works based on queue, and user can choose to upload multiple files in the activity. The first file always gets uploaded, but the files after always return with the crash.
I finally managed to fix this. Apparently it is because the permission for the given uri is only valid as long as the receiving activity is active. So, sending the uri to a background service (upload service) will result in SecurityException as expected, unless the uri is a persistable uri (ie. from ACTION_OPEN_DOCUMENT).
So my solution is to copy the file to a file my app created and use FileProvider.getUriForFile() function to get the uri and send it instead to the background service and delete the copy when my service finished uploading. This works fine even after the calling activity has finished.
The following code is working fine in my device (android 7):
public void pickImage(View view) {
try {
Intent photoPickerIntent = new Intent(Intent.ACTION_GET_CONTENT);
photoPickerIntent.setType("image/*");
photoPickerIntent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
startActivityForResult(photoPickerIntent, RC_PICK_IMAGE);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
}
}
onActivityResult:
Uri uri = data.getData();
InputStream in = getContentResolver().openInputStream(uri);

How to have the Camera Intent put a photo into internal storage?

I have been at this all day and can't seem to get it to work. It use to work before according to the previous person who worked on it.
cameraIntent = new Intent("android.media.action.IMAGE_CAPTURE");
String imageName = CustomApp.getTimeStamp(0) ;
String path = CustomApp.getCurrentActivity().getDir("images", Context.MODE_WORLD_WRITEABLE).getPath()+File.separator+imageName;
File file = new File(path) ;
Uri img = Uri.fromFile(file) ;
Intent passthruDataIntent = new Intent();
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, img);
CustomApp.getCurrentActivity().startActivityForResult(cameraIntent, CustomConstants.REQUESTCODE_CAMERA);
Similar code has been posted on here, but it doesn't seem to work on my nexus 4 on 4.2.2. I tried external storage and it works fine then. Any insight on why it might not be working would be very helpful. Thanks.
Internal storage is private for each app -- the third-party camera app has no rights to write to your internal storage.
I was fighting with the same problem and got a solution much later that the question was asked, but I believe this might be useful to the community.
With the new dynamic permissions introduced in Android 6.0 / API level 23 the topic question has become particularly important, since you need to request the permissions at runtime and handle the both accepting and rejecting reactions of the user. To use the camera activity you need to ask for the corresponding permission first (android.permission.CAMERA). Then, if you store the picture in an external directory, the corresponding permission android.permission.READ_EXTERNAL_STORAGE also needed to be granted to your app by the user. A runtime permission request seems natural to the user at the moment when the user is about to perform the intended action (e.g., if the camera access permission request appears just after the button "Take photo" is pressed). However, if you use the external storage to save the camera picture, you need to ask at the same time for two permissions when your app takes a photo: (1) use the camera and (2) access the external storage. The latter might be frustrating since it is not necessarily clear why your app tries to reach the user files while the user expects just a photo to be taken.
The solution allowing to avoid the external storage and to save the camera picture directly consists in using the content providers. According to the storage options documentation,
Android provides a way for you to expose even your private data to other applications — with a content provider. A content provider is an optional component that exposes read/write access to your application data, subject to whatever restrictions you want to impose.
This is exactly what you need to allow to the camera activity to save the picture directly into the local storage of your app, so that you can easily access it then without requesting additional permissions (only the camera access needed to be granted).
A good article with a code example is provided here. The following generalized code inspired by this article is used in our app to do the trick.
The content provider class:
/**
* A content provider that allows to store the camera image internally without requesting the
* permission to access the external storage to take shots.
*/
public class CameraPictureProvider extends ContentProvider {
private static final String FILENAME = "picture.jpg";
private static final Uri CONTENT_URI = Uri.parse("content://xyz.example.app/cameraPicture");
#Override
public boolean onCreate() {
try {
File picture = new File(getContext().getFilesDir(), FILENAME);
if (!picture.exists())
if (picture.createNewFile()) {
getContext().getContentResolver().notifyChange(CONTENT_URI, null);
return true;
}
} catch (IOException | NullPointerException e) {
e.printStackTrace();
}
return false;
}
#Nullable
#Override
public ParcelFileDescriptor openFile(#NonNull Uri uri, #NonNull String mode) throws FileNotFoundException {
try {
File picture = new File(getContext().getFilesDir(), FILENAME);
if (!picture.exists())
picture.createNewFile();
return ParcelFileDescriptor.open(picture, ParcelFileDescriptor.MODE_READ_WRITE);
} catch (IOException | NullPointerException e) {
e.printStackTrace();
}
return null;
}
#Nullable
#Override
public Cursor query(#NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
#Nullable
#Override
public String getType(#NonNull Uri uri) {
String lc = uri.getPath().toLowerCase();
if (lc.endsWith(".jpg") || lc.endsWith(".jpeg"))
return "image/jpeg";
return null;
}
#Nullable
#Override
public Uri insert(#NonNull Uri uri, ContentValues values) {
return null;
}
#Override
public int delete(#NonNull Uri uri, String selection, String[] selectionArgs) {
return 0;
}
#Override
public int update(#NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
}
The content provider is needed to be declared in the app manifest:
<provider android:authorities="xyz.example.app"
android:enabled="true"
android:exported="true"
android:name="xyz.example.app.CameraPictureProvider" />
Finally, to use the content provider in order to capture the camera picture, the following code is invoked from a calling activity:
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// Ensure that there's a camera activity to handle the intent
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, CameraPictureProvider.CONTENT_URI);
startActivityForResult(takePictureIntent, 0);
Please note that the camera permission request needed to be handled separately (it is not done in the presented code sample).
It is also worth noticing that the permission requests needed to be handled only if you are using build tools version 23 or higher. The same code is compatible with lower-level build tools, and is useful in case you are not bothered by the runtime permission requests but just want to avoid using the external storage.
I had the same problem. I solved it by first saving the photos on external memory and then copied to internal memory. Hope this helps.

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