Open PDF after download with DownloadManager and FileProvider on Android Q (10) - java

targetSdkVersion: 30
In our App we have a feature, where we download files (mostly pdf) to the public download folder and start an intent afterwards to open it. Our code works fine for android apps with api >= 28 and >= 30. Just our app on Android 10 (sdkVersion 29) will try to open the document and instantly closes the activity that tried to display the pdf. The logcat shows following error:
22-03-17 14:23:42.486 12161-15168/? E/DisplayData: openFd: java.io.FileNotFoundException: open failed: EACCES (Permission denied)
22-03-17 14:23:42.486 12161-15168/? E/PdfLoader: Can't load file (doesn't open) Display Data [PDF : download.pdf] +ContentOpenable, uri: content://com.example.fileprovider/Download/download.pdf
If I grant the files permission in the app settings, it will work flawlessly but if I understood the android documentation correctly this should not be necessary since Android 10. Especially because there are no problems on Android 11 and Android 12 devices that do not have this permission. On all Android versions the file will be downloaded correctly and the user could manually open it from the download section of his device.
This is the Android Manifest section for the fileprovider
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/filepaths" />
</provider>
The filepaths XML file
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path
name="external_files"
path="." />
<external-path
name="Download"
path="Download/"/>
<files-path
name="files"
path="." />
<external-cache-path
name="external_cache"
path="." />
</paths>
This is the code with the DownloadManager usage to download the files
public static long downloadFile(Context context, String url, String fileName) {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
try {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setTitle(fileName)
.setDescription(Localization.getStringClient("file_download_progress"))
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
return downloadManager.enqueue(request);
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(context, Localization.getStringClient("error_downloading_asset"), Toast.LENGTH_SHORT).show();
}
return -1;
}
The broadcast receiver that listens on the download progress
public class DownloadBroadcastReceiver extends BroadcastReceiver {
private long downloadId = -2;
public void setDownloadId(long downloadId) {
this.downloadId = downloadId;
}
#Override
public void onReceive(Context context, Intent intent) {
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
if (id == downloadId) {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE);
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(id);
Cursor c = downloadManager.query(query);
if (c != null) {
c.moveToFirst();
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS);
if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) {
String uriString = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
String mediaType = c.getString(c.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE));
Uri fileUri = Uri.parse(uriString);
DownloadUtils.openFile(context, fileUri, mediaType);
} else if (DownloadManager.STATUS_FAILED == c.getInt(columnIndex)) {
Toast.makeText(context, Localization.getStringClient("error_downloading_asset"), Toast.LENGTH_SHORT).show();
}
c.close();
}
}
}
}
The code to open the file. Only on Android 10 the app closes the activity instantly again.
public static void openFile(Context context, Uri fileUri, String mediaType) {
File file = new File(fileUri.getPath());
Uri uri = FileProvider.getUriForFile(
context,
context.getApplicationContext().getPackageName() + ".fileprovider",
file
);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, mediaType);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
Toast.makeText(context, Localization.getStringClient("open_file_no_activity"), Toast.LENGTH_SHORT).show();
}
}
And a download call in the looks like this
#Override
public void startPDFDownload(String pdfDownloadUrl, String fileName) {
long downloadId = DownloadUtils.downloadFile(requireContext(), pdfDownloadUrl, fileName);
if (downloadId > -1) {
DownloadBroadcastReceiver receiver = new DownloadBroadcastReceiver();
receiver.setDownloadId(downloadId);
requireContext().registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
}
I think I have a false understanding of how file handling works in Android 10 but I do not know where I have to adjust the code or configuration. Help is really appreciated. Currently as a workaround we ask for the permission for WRITE_EXTERNAL_STORAGE to open downloaded files on Android 10. But I would prefer to do it the right way.
Solution:
I adjusted the BroadcastReceiver to the following code. I removed the LOCAL_URI from the cursor and used the URI from the DownloadManager method.
public class DownloadBroadcastReceiver extends BroadcastReceiver {
private long downloadId = -2;
public void setDownloadId(long downloadId) {
this.downloadId = downloadId;
}
#Override
public void onReceive(Context context, Intent intent) {
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
if (id == downloadId) {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE);
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(id);
Cursor c = downloadManager.query(query);
if (c != null) {
c.moveToFirst();
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS);
if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) {
String mediaType = c.getString(c.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE));
DownloadUtils.openFile(context, downloadManager.getUriForDownloadedFile(id), mediaType);
} else if (DownloadManager.STATUS_FAILED == c.getInt(columnIndex)) {
Toast.makeText(context, Localization.getStringClient("error_downloading_asset"), Toast.LENGTH_SHORT).show();
}
c.close();
}
}
}
}
And I adjusted the method to open the file with the uri to following code. I removed the FileProvider code and used the uri from the DownloadManager.
public static void openFile(Context context, Uri fileUri, String mediaType) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(fileUri, mediaType);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
Toast.makeText(context, Localization.getStringClient("open_file_no_activity"), Toast.LENGTH_SHORT).show();
}
}
The code of the other methods remains the same.

You should not use FileProvider to obtain an uri for your file.
You can get an uri from DownloadManager and use it to serve your file.
Code would be the same for all Android versions.
Not a single permission needed.

Related

Save image using MediaStore, saved with wrong name and extension

I'm trying to use MediaStore API to save an image but it always save the file with this name
1637955326427.jpg and the size of the file is 0 B.
this is the code that i use:
OutputStream fos;
ContentResolver resolver = getBaseContext().getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, "fileName.jpg");
contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
try {
fos = resolver.openOutputStream(Objects.requireNonNull(imageUri));
Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(),list.get(0).getDocfile().getUri());
if(bitmap!=null){
bitmap.compress(Bitmap.CompressFormat.JPEG,100,fos);
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
list.get(0).getDocFile() return a DocumentFile also i tested this on API 28
Thanks in advance
NOTICE this image already exist on the External Storage, just need to copy it.
EDIT: I delete this code fos = resolver.openOutputStream(imageUri); was duplucate. I do realy apologize. the file work fine but steal wrong name.
First lets take a look at getting write permissions for an existing image. Please note that the photopath does not necessarily exist yet but should point to the location you want your file to be saved.
if (!requestPhotoWritePermission(this, getImageUriFromFS(this, new File(photoPath)))) {
return;
} else {
takePicture();
}
This will form an overwrite request with the default camera
public static Uri getImageUriFromFS(Context c, File file) {
long id = getFilePathToImageID(file, c);
Uri fromUri = ContentUris.withAppendedId( MediaStore.Images.Media.EXTERNAL_CONTENT_URI,id);
return fromUri;
}
public static long getFilePathToImageID(File imagePath, Context context)
{
Uri mainUri;
Cursor cursor1 = context.getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Images.Media._ID},
MediaStore.Images.Media.DATA + "=? ",
new String[]{imagePath.getAbsolutePath()}, null);
long id = 0;
if (cursor1 != null && cursor1.moveToFirst()) {
id = cursor1.getLong(cursor1.getColumnIndex(MediaStore.MediaColumns._ID));
cursor1.close();
}
return id;
}
public static boolean requestPhotoWritePermission(Activity activity, Uri fromUri) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
boolean hasPermission = true;
if (activity.checkUriPermission(fromUri, Binder.getCallingPid(), Binder.getCallingUid(),
Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION) != PackageManager.PERMISSION_GRANTED) {
hasPermission = false;
}
List<Uri> uriList = new ArrayList<>();
uriList.add(fromUri);
if (!hasPermission) {
PendingIntent pi = MediaStore.createWriteRequest(activity.getContentResolver(), uriList);
try {
activity.startIntentSenderForResult(pi.getIntentSender(), REQUEST_OVERWRITE_PERMISSION_CODE, null, 0, 0, 0);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
return false;
}
return true;
}
return true;
}
This will persist the permission to overwrite or create a new picture via the camera intent
Then in your activity:
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 53) {
if (resultCode == RESULT_OK) {
//you'll want to take your picture here and possibly store the uri to your photo for later retrieval
takePicture();
}
}
This will take a picture if the permission request is accepted
To Take a picture using the default intent with a provider:
Declare your provider in the manifest
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:enabled="true"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/file_paths">
</meta-data>
</provider>
1.5: xml File paths file:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path
name="internal_images"
path="files/Pictures" />
<external-files-path
name="internal_images_alternate"
path="Pictures" />
<external-path
name="external"
path="Documents" />
<external-files-path
name="external_files"
path="Documents" />
<cache-path
name="cache"
path="Documents" />
<external-cache-path
name="external_cache"
path="Documents" />
<files-path
name="files"
path="Documents" />
</paths>
The above will give you permission to save to the documents folder. Replace as required.
Utilize the provider to take the picture
public static File dispatchTakePictureIntent(Activity activity, String photoPath) {
Intent takePictureIntent = getCameraIntentWithUpdatedPackages(activity);
Uri photoURI = Uri.fromFile(new File(photoPath));
Uri photoUri = FileProvider.getUriForFile(activity.getApplicationContext(), activity.getApplicationContext().getPackageName() + ".provider", new File(photoPath));
activity.grantUriPermission(photoURI.getAuthority(), photoUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
//disable strict mode policies
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
activity.startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
return new File(photoPath);
return null;
}
EDIT:
I forgot something
public static Intent getCameraIntentWithUpdatedPackages(Context context) {
List<ResolveInfo> resolveInfo = new ArrayList<>();
final Intent capturePhoto = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
PackageManager pm = context.getPackageManager();
resolveInfo = pm.queryIntentActivities(capturePhoto, 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// For Android 11 we need to add specific camera apps
// due them are not added during ACTION_IMAGE_CAPTURE scanning...
// resolveInfo.addAll(getCameraSpecificAppsInfo(context));
}
capturePhoto.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
return capturePhoto;
}
EDIT: Update by the name (in onActivityResult after requesting an overwrite permission)
Uri fromUri = MediaUtils.getImageUriFromFS(this, new File(from));
ContentResolver contentResolver = getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 1);
contentResolver.update(fromUri, contentValues, null, null);
contentValues.clear();
contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, new File(to).getName());
contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 0);
contentResolver.update(fromUri, contentValues, null, null);
I delete this code fos = resolver.openOutputStream(imageUri); was duplicate. the image work fine but steal wrong name.
About the Wrong name and extension, i think that MediaStore doesn't work fine with Android 9 and lower, because i tested the same code on Android 11 and it's working fine

Can't install apk from uri on Android 10 in Eclipse projects

We are installing apk from file uri, its working upto Android Pie(9) ,in Android 10 mobile its showing "There was problem while Parsing". we have stored file in the application storage only and the build version is 4.4
I have shared the code below.
String PATH = Objects.requireNonNull(this.myActivity.getExternalFilesDir(null)).getAbsolutePath();
File file = new File(PATH + "/"+Utils.apk_name);
intent = new Intent("android.intent.action.VIEW");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri uri = FileProvider.getUriForFile(this.myActivity, this.myActivity.getApplicationContext().getPackageName() + ".provider", file);
//Intrinsics.checkExpressionValueIsNotNull(uri, "FileProvider.getUriForFi…eProvider.install\", file)");
intent.setDataAndType(uri, "application/vnd.android.package-archive");
startActivity(intent);
Provider in Manifest
<provider
android:name="com.kirubha.helpers.ESEProvider"
android:authorities="com.kirubha.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/provider_paths" />
</provider>
provider_paths
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external_storage_root"
path="."/>
</paths>
I have found most of my file provider worries disapear when I add
StrictMode.VmPolicy.Builder buildervm = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(buildervm.build());
to my main activities.
The above was found in this answer which might be useful to you.
android.os.FileUriExposedException: file:///storage/emulated/0/test.txt exposed beyond app through Intent.getData()
In Android 10, ACTION_VIEW is deprecated, need to use PackageInstaller instead.
Follow the below code to implement PackageInstaller in Java.
private static int REQUEST_OEN_DOCUMENT = 1337;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent intent1 = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent1.setType("application/vnd.android.package-archive");
intent1.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent1, REQUEST_OPEN_DOCUMENT);
}
This code will redirect to the files folder. Then need to select the .apk file.
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_OEN_DOCUMENT) {
if (resultCode == Activity.RESULT_OK) {
if (data != null) {
Uri uri = data.getData();
installAfterQ(uri);
}
}
}
}
public void installAfterQ(Uri uri) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
InputStream inputStream = getApplication().getContentResolver().openInputStream(uri);
long length = DocumentFile.fromSingleUri(getApplication(), uri).length();
PackageInstaller installer = getApplication().getPackageManager().getPackageInstaller();
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
int sessionId = installer.createSession(params);
PackageInstaller.Session session = installer.openSession(sessionId);
File file = new File(uri.getPath());
OutputStream outputStream = session.openWrite(file.getName(), 0, length);
byte[] buf = new byte[1024];
int len;
while ((len = inputStream.read(buf)) > 0) {
outputStream.write(buf, 0, (int) len);
}
session.fsync(outputStream);
outputStream.close();
inputStream.close();
Intent intent = new Intent(getApplication(), UpdateReceiver.class);
PendingIntent pi = PendingIntent.getBroadcast(getApplication(), 3439, intent, PendingIntent.FLAG_UPDATE_CURRENT);
session.commit(pi.getIntentSender());
session.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
Update Receiver Class:
public class UpdateReceiver extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1);
if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
Intent activityIntent =intent.getParcelableExtra(Intent.EXTRA_INTENT);
context.startActivity(activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} else if (status == PackageInstaller.STATUS_SUCCESS) {
Toast.makeText(context, "Successfully Installed", Toast.LENGTH_SHORT).show();
} else {
String msg =intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
Toast.makeText(context, "Error while installing" + msg, Toast.LENGTH_SHORT).show();
}
}
}
Android Manifest.xml
Inside Application tag.
<receiver android:name=".UpdateReceiver"/>
<meta-data
android:name="com.android.packageinstaller.notification.smallIcon"
android:resource="#drawable/ic_install_notification" />
Permission:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
Try this code. Hope this code is helpful.

How to retrieve and open PDF file saved to downloads through MediaStore API in Android?

I am downloading a PDF file from a server and passing the response body bytestream into the function below, which is storing the PDF file successfully in the user downloads folder.
#RequiresApi(Build.VERSION_CODES.Q)
fun saveDownload(pdfInputStream: InputStream) {
val values = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, "test")
put(MediaStore.Downloads.MIME_TYPE, "application/pdf")
put(MediaStore.Downloads.IS_PENDING, 1)
}
val resolver = context.contentResolver
val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val itemUri = resolver.insert(collection, values)
if (itemUri != null) {
resolver.openFileDescriptor(itemUri, "w").use { parcelFileDescriptor ->
ParcelFileDescriptor.AutoCloseOutputStream(parcelFileDescriptor)
.write(pdfInputStream.readBytes())
}
values.clear()
values.put(MediaStore.Downloads.IS_PENDING, 0)
resolver.update(itemUri, values, null, null)
}
}
Now once this function returns I want to open the saved PDF file. I've tried several ways to get this to work but the pickers always say that there is nothing to open the file. I think that there is either still a permissions issue going on (maybe I'm using the FileProvider wrong?), or perhaps the path is wrong, or it could be something else entirely.
Here's a couple of examples of what I've tried:
fun uriFromFile(context: Context, file: File): Uri {
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
}
a)
val openIntent = Intent(Intent.ACTION_VIEW)
openIntent.putExtra(Intent.EXTRA_STREAM, uriFromFile(this, File(this.getExternalFilesDir(DIRECTORY_DOWNLOADS)?.absolutePath.toString(), "test")))
openIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
openIntent.type = "application/pdf"
startActivity(Intent.createChooser(openIntent, "share.."))
b)
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(Intent.EXTRA_STREAM, uriFromFile(this, File(this.getExternalFilesDir(null)?.absolutePath.toString(), "test.pdf")))
shareIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
shareIntent.type = "application/pdf"
startActivity(Intent.createChooser(shareIntent, "share.."))
c)
val file = File(itemUri.toString()) //itemUri from the saveDownload function
val target = Intent(Intent.ACTION_VIEW)
val newFile = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".provider", file);
target.setDataAndType(newFile, "application/pdf")
target.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
val intent = Intent.createChooser(target, "Open File")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
ContextCompat.startActivity(this, intent, null)
d)
val target = Intent(Intent.ACTION_VIEW)
target.setDataAndType(Uri.parse("content://media/external_primary/downloads/2802"), "application/pdf"
target.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
val intent = Intent.createChooser(target, "Open File")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
ContextCompat.startActivity(this, intent, null)
(also tried /test.pdf on the end of this URI, and replacing media with my authority name)
I have also added this to my manifest file within the application tags:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/provider_paths" />
</provider>
#xml/provider_paths is as follows, although I have tried various combinations in addition to this including the paths as ".":
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path name="files_root" path="/"/>
<files-path name="files_root" path="/"/>
<external-path name="files_root" path="/"/>
</paths>
As a side note, there is definitely pickers available capable of opening PDFs, and going into the file explorer and opening it from there works fine. When attempting to share instead of opening the sharing also fails.
Follow this step and code, it will manage everything from downloading your pdf and opening it.
Create a class name as DownloadTask and put the complete code given below
public class DownloadTask {
private static final String TAG = "Download Task";
private Context context;
private String downloadFileUrl = "", downloadFileName = "";
private ProgressDialog progressDialog;
long downloadID;
private BroadcastReceiver onDownloadComplete = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
//Fetching the download id received with the broadcast
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
//Checking if the received broadcast is for our enqueued download by matching download id
if (downloadID == id) {
downloadCompleted(downloadID);
}
}
};
public DownloadTask(Context context, String downloadUrl) {
this.context = context;
this.downloadFileUrl = downloadUrl;
downloadFileName = downloadFileUrl.substring(downloadFileUrl.lastIndexOf('/') + 1);//Create file name by picking download file name from URL
Log.e(TAG, downloadFileName);
context.registerReceiver(onDownloadComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
downloadFile(downloadFileUrl);
}
public void downloadFile(String url) {
try {
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(), downloadFileName);
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url))
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)// Visibility of the download Notification
.setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS,
downloadFileName
)
.setDestinationUri(Uri.fromFile(file))
.setTitle(downloadFileName)// Title of the Download Notification
.setDescription("Downloading")// Description of the Download Notification
.setAllowedOverMetered(true)// Set if download is allowed on Mobile network
.setAllowedOverRoaming(true);// Set if download is allowed on roaming network
request.allowScanningByMediaScanner();
DownloadManager downloadManager = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE);
downloadID = downloadManager.enqueue(request);// enqueue puts the download request in the queue.
progressDialog = new ProgressDialog(context);
progressDialog.setMessage("Downloading...");
progressDialog.setCancelable(false);
progressDialog.show();
} catch (Exception e) {
Log.d("Download", e.toString());
}
}
void downloadCompleted(long downloadID) {
progressDialog.dismiss();
new AlertDialog.Builder(context)
.setTitle("Document")
.setMessage("Document Downloaded Successfully")
.setPositiveButton("Open", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
openDownloadedAttachment(downloadID);
}
})
// A null listener allows the button to dismiss the dialog and take no further action.
.setNegativeButton(android.R.string.no, null)
.setIcon(android.R.drawable.ic_dialog_alert)
.show();
context.unregisterReceiver(onDownloadComplete);
}
Uri path;
private void openDownloadedAttachment(final long downloadId) {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(downloadId);
Cursor cursor = downloadManager.query(query);
if (cursor.moveToFirst()) {
int downloadStatus = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
String downloadLocalUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
String downloadMimeType = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE));
if ((downloadStatus == DownloadManager.STATUS_SUCCESSFUL) && downloadLocalUri != null) {
path = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", new File(Uri.parse(downloadLocalUri).getPath()));
//path = Uri.parse(downloadLocalUri);
Intent pdfIntent = new Intent(Intent.ACTION_VIEW);
pdfIntent.setDataAndType(path, downloadMimeType);
pdfIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_CLEAR_TOP);
try {
context.startActivity(pdfIntent);
} catch (ActivityNotFoundException e) {
Toast.makeText(context, "No Application available to view PDF", Toast.LENGTH_SHORT).show();
}
}
}
cursor.close();
}
}
And then download your pdf like this from your activity.
new DownloadTask(this, "PDF_URL");
And from your fragment
new DownloadTask(getContext(), "PDF_URL");
After download completed it will open your pdf automatically.
According to Android Developer, MediaStore isn't being used for accessing non-media files such as pdf files:
If your app works with documents and files that don't exclusively
contain media content, such as files that use the EPUB or PDF file
extension, use the ACTION_OPEN_DOCUMENT intent action, as described in
the guide on how to store and access documents and other files.
Moreover, there isn't any official solution to access non-media files by means of using Cursor and Content Provider. However, there is an official and clean code approach which I've tested it on Android 11 and worked as expected. here is:
public class retrieve_pdf_file {
#RequiresApi(Build.VERSION_CODES.Q)
public static void get(Activity activity) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/pdf");
// Optionally, specify a URI for the file that should appear in the
// system file picker when it loads.
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI);
activity.startActivityForResult(intent, main_activity.PICK_PDF_FILE);
}
public static void get(Activity activity, String filename) { // filename is used for lower that API level 29
// older that API level 29 approaches
File file = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
// TODO
}
}
And also, to get the selected pdf file's Uri you must listen for the activity's result:
#Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
if (requestCode == PICK_PDF_FILE && resultCode == Activity.RESULT_OK) {
System.out.println("request code: PICK_PDF_FILE && result code: OK");
// The result data contains a URI for the document or directory that
// the user selected.
Uri uri = null;
if (resultData != null) {
uri = resultData.getData();
// Perform operations on the document using its URI.
System.out.println(uri);
} else {
System.out.println("resultData is null");
}
} else {
System.out.println("result code: NOT OK");
}
}
This is the official solution that can be found in Android Developer for API level 29 or higher.
Here is the code that i use to open doc file with Uri.
fun viewPDFIntent(fileUri: Uri?, context: Context, title: String?, type: String) {
val viewPDFIntent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(fileUri, type)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
context.startActivity(Intent.createChooser(viewPDFIntent, title))
}
Here type for pdf is "application/pdf".
You are getting created pdf uri in itemUri variable, pass this to first argument of this function.

Android: Cannot add attachment: `SecurityException when openAssetFileDescriptor`

I am unable to attach my file in native android email or Gmail application.
Gmail gives me this error:
2019-01-17 16:33:17.884 15415-15415/? E/Gmail: Gmail:Error adding attachment
fjd: SecurityException when openAssetFileDescriptor.
That is the code where save the file on storage:
public Uri getUrlFromDrawable(String base64ImageData) {
FileOutputStream fos;
try {
final String pureBase64Encoded = base64ImageData.substring(base64ImageData.indexOf(",") + 1);
final byte[] decodedBytes = Base64.decode(pureBase64Encoded, Base64.DEFAULT);
String filename = "receipt2_" + System.currentTimeMillis() + ".jpg";
File file = new File(reactContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES), filename);
fos = new FileOutputStream(file);
fos.write(decodedBytes);
fos.flush();
fos.close();
Uri contentUri = FileProvider.getUriForFile(getReactApplicationContext(), "com.myapp.fileprovider", file);
return contentUri;
}
return null
}
And here is my function to send the mail:
public void mail(ReadableMap options, Callback callback) {
Intent i = new Intent(Intent.ACTION_SENDTO);
i.setData(Uri.parse("mailto:"));
if (options.hasKey("subject") && !options.isNull("subject")) {
i.putExtra(Intent.EXTRA_SUBJECT, options.getString("subject"));
}
if (options.hasKey("body") && !options.isNull("body")) {
String body = options.getString("body");
i.putExtra(Intent.EXTRA_TEXT, Html.fromHtml(new StringBuilder().append(body).toString())
);
}
if (options.hasKey("recipients") && !options.isNull("recipients")) {
ReadableArray recipients = options.getArray("recipients");
i.putExtra(Intent.EXTRA_EMAIL, readableArrayToStringArray(recipients));
}
if (options.hasKey("receipt") && !options.isNull("receipt")) {
String base64ImageData = options.getString("receipt");
try {
if (base64ImageData != null) {
Uri imageUri = getUrlFromDrawable(base64ImageData);
i.putExtra(Intent.EXTRA_STREAM, imageUri);
}
} catch (Exception e) {
Log.e("ERROR", "Cannot save receipt");
}
}
PackageManager manager = reactContext.getPackageManager();
List<ResolveInfo> list = manager.queryIntentActivities(i, 0);
if (list == null || list.size() == 0) {
callback.invoke("not_available");
return;
}
if (list.size() == 1) {
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
reactContext.startActivity(i);
} catch (Exception ex) {
callback.invoke("error");
}
} else {
Intent chooser = Intent.createChooser(i, "Send Mail");
chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
reactContext.startActivity(chooser);
} catch (Exception ex) {
callback.invoke("error");
}
}
}
Here is the fileprovider.xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path
name="Pictures"
path="/" />
</paths>
And here is the provider in `AndroidManifest.xml
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.myapp.fileprovider"
tools:replace="android:authorities"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
tools:replace="android:resource"
android:resource="#xml/fileprovider" />
</provider>
I managed to fix it by granting permission at runtime like this
List<ResolveInfo> list = manager.queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : list) {
String packageName = resolveInfo.activityInfo.packageName;
reactContext.grantUriPermission(packageName, imageUri , Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
If you also add it as ClipData, the permission grant will work successfully:
yourIntent.clipData = ClipData.newUri(context.contentResolver, fileName, contentUri)
The culprit is that Intent.FLAG_GRANT_READ_URI_PERMISSION will work only for Intent.data and Intent.clipData, and not for extras, unless the uri permission grant has been given explictly (inconvenient), or has been given by also adding a ClipData.
Note that your answer also worked, though I find it a little less convenient since for Android 11+, it requires you to declare queries in the AndroidManifest.xml, which might also be a privacy issue in some cases.

Picking an image from gallery gives error Android N

I am having trouble with retrieving the Uri from an intent in Android N.
As far as I know on Android 24 and above to get an external Uri you need FileProvider declared in Manifest. That's all done and it works with the camera, but when I try to get an image from the gallery I get an error in onActivityResult data.getData();
These are a few samples of my code:
public void getPictureFromGallery(){
picUriCar = null;
try {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
startActivityForResult(intent, PIC_SELECT);
} catch (ActivityNotFoundException e) {
Toast.makeText(MainActivity.this, getResources().getString(R.string.text_error_no_gallery_app),
Toast.LENGTH_SHORT).show();
}
}
And onActivityResult:
else if(requestCode == PIC_SELECT){
picUriCar = data.getData();
if (picUriCar != null){
performCrop();
}
}
As far as I know data.getData() returns a Uri and this works ok on Marshmallow, but on a Nougat phone i get this error:
java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=5, result=-1, data=Intent {
dat=content://com.android.externalstorage.documents/document/4996-1EFF:DCIM/100ANDRO/DSC_0004.JPG
flg=0x1 }} to activity
{com.company.example/com.company.example.MainActivity}:
java.lang.SecurityException: Uid 10246 does not have permission to uri
0 #
content://com.android.externalstorage.documents/document/4996-1EFF%3ADCIM%2F100ANDRO%2FDSC_0004.JPG
at android.app.ActivityThread.deliverResults(ActivityThread.java:4267)
at android.app.ActivityThread.handleSendResult(ActivityThread.java:4310)
at android.app.ActivityThread.-wrap20(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1628)
at android.os.Handler.dispatchMessage(Handler.java:110)
at android.os.Looper.loop(Looper.java:203)
at android.app.ActivityThread.main(ActivityThread.java:6361)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1063)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:924)
Caused by: java.lang.SecurityException: Uid 10246 does not have permission to uri 0 #
content://com.android.externalstorage.documents/document/4996-1EFF%3ADCIM%2F100ANDRO%2FDSC_0004.JPG
at android.os.Parcel.readException(Parcel.java:1683)
at android.os.Parcel.readException(Parcel.java:1636)
at android.app.ActivityManagerProxy.startActivity(ActivityManagerNative.java:3213)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1525)
at android.app.Activity.startActivityForResult(Activity.java:4235)
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:767)
at android.app.Activity.startActivityForResult(Activity.java:4194)
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:754)
at com.company.example.MainActivity.performCrop(MainActivity.java:1654)
at com.company.example.MainActivity.onActivityResult(MainActivity.java:1534)
at android.app.Activity.dispatchActivityResult(Activity.java:6928)
at android.app.ActivityThread.deliverResults(ActivityThread.java:4263)
at android.app.ActivityThread.handleSendResult(ActivityThread.java:4310) 
at android.app.ActivityThread.-wrap20(ActivityThread.java) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1628) 
at android.os.Handler.dispatchMessage(Handler.java:110) 
at android.os.Looper.loop(Looper.java:203) 
at android.app.ActivityThread.main(ActivityThread.java:6361) 
at java.lang.reflect.Method.invoke(Native Method) 
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1063) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:924)
My question is:
How do I pass data.getData() uri to picUriCar without any errors?
Use this code to create new intent to choose image:
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
intent.setType(image/*);
startActivityForResult(Intent.createChooser(intent, ""), Constants.SELECT_PICTURE);
Use this code in onActivityResult:
if (resultCode == Activity.RESULT_OK) {
if (requestCode == Constants.SELECT_PICTURE) {
Uri selectedImageUri = data.getData();
try {
if (isNewGooglePhotosUri(selectedImageUri)) {
resultFile = getPhotoFile(selectedImageUri);
} else {
resultFile = getFilePathForGallery(selectedImageUri);
}
if (resultFile == null) {
//error
return;
}
} catch (Exception e) {
e.printStackTrace();
//error
return;
}
}
}
}
Also here is some usefull function that i use in my code:
private File getFilePathForGallery(Uri contentUri) {
String path = null;
String[] proj = {MediaStore.Images.Media.DATA};
Cursor cursor = getContentResolver().query(contentUri, proj, null, null, null);
if (cursor.moveToFirst()) {
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
path = cursor.getString(column_index);
}
cursor.close();
return new File(path);
}
public static boolean isNewGooglePhotosUri(Uri uri) {
return "com.google.android.apps.photos.contentprovider".equals(uri.getAuthority());
}
private File getPhotoFile(Uri selectedImageUri) {
try {
InputStream is = mActivityInstance.getContentResolver().openInputStream(selectedImageUri);
if (is != null) {
Bitmap pictureBitmap = BitmapFactory.decodeStream(is);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
pictureBitmap.compress(Bitmap.CompressFormat.JPEG, 80, bytes);
File output = new File(FileManager.getImageCacheDir(mActivityInstance), System.currentTimeMillis() + ".jpg");
output.createNewFile();
FileOutputStream fo = new FileOutputStream(output);
fo.write(bytes.toByteArray());
fo.close();
return output;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
Functions from FileManager class:
private static File getCacheDir(Context context) {
File cacheDir = context.getExternalFilesDir(null);
if (cacheDir != null) {
if (!cacheDir.exists())
cacheDir.mkdirs();
} else {
cacheDir = context.getCacheDir();
}
return cacheDir;
}
public static File getImageCacheDir(Context context) {
File imageCacheDir = new File(getCacheDir(context), "cache_folder");
if (!imageCacheDir.exists())
imageCacheDir.mkdirs();
return imageCacheDir;
}
Also you need to create new xml file in your xml folder:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external_files"
path="." />
</paths>
and then add new provider to manifest file:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/your_xml_file" />
</provider>
I did a few checks and as always the solution was simpler that I expected. This is my performCrop function. I have two options when clicking on a View. Either get the image from the gallery or capture it with the camera. I remembered that the gallery option problem occurred only after I fixed the camera problem. So the issue might be somewhere within the crop code. So this is what I did:
public void performCrop(boolean cameraShot){
try {
Intent cropIntent = new Intent("com.android.camera.action.CROP");
//indicate image type and Uri
//cropIntent.setDataAndType(picUri, "image");
cropIntent.setDataAndType(picUriCar, "image/*");
//set crop properties
cropIntent.putExtra("crop", "true");
//indicate aspect of desired crop
cropIntent.putExtra("aspectX", 2);
cropIntent.putExtra("aspectY", 1);
//indicate output X and Y
cropIntent.putExtra("outputX", 1024);
cropIntent.putExtra("outputY", 512);
cropIntent.putExtra("scale", true);
cropIntent.putExtra("return-data", false);
Uri uri;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || !cameraShot) {
uri = Uri.fromFile(croppedFileCar);
}else{
String authority = "com.company.example.provider";
uri = FileProvider.getUriForFile(
MainActivity.this,
authority,
croppedFileCar);
cropIntent.setClipData(ClipData.newRawUri( "", uri ) );
cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
Please notice - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
!cameraShot)
So basically the logic behind it is that if the image is not provided by the camera (which is determined in onActivityResult) or API is below 24, execute the line:
uri = Uri.fromFile(croppedFileCar);
Otherwise use the FileProvider. Before I did these changes the app crashed when picking an image from the gallery. I have no idea why it works now, but if someone knows the answer, I would be happy to hear it.

Categories