As part of my app, users can select images from their device and I store the URLs to these in my database. Then, elsewhere in the app, I need to display those images.
I am getting the image URI through Intent.ACTION_PICK:
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
imagePickerAction = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
if (uri != null) {
val uriString = uri.toString()
saveToDatabase(uriString)
}
}
This results in a URI string like content://com.android.providers.media.documents/document/image%3A1000000038.
I am able to show the image using ImageView.setImageURI at this point.
But later on, when I fetch the URI from the database and try to use setImageURI again, I get this error:
java.lang.SecurityException: Permission Denial: opening provider com.android.providers.media.MediaDocumentsProvider from ProcessRecord{cdd4e32 10499:com.skamz.shadercam/u0a160} (pid=10499, uid=10160) requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs
I have also tried following the docs here https://developer.android.com/training/data-storage/shared/documents-files#open:
private fun getBitmapFromUri(uri: Uri): Bitmap {
val parcelFileDescriptor: ParcelFileDescriptor? =
contentResolver.openFileDescriptor(uri, "r")
if (parcelFileDescriptor == null) {
return bitmapFromUri(contentResolver, Uri.parse(TextureOverlayShaderData.defaultImageUrl))
}
val fileDescriptor: FileDescriptor = parcelFileDescriptor.fileDescriptor
val image: Bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor)
parcelFileDescriptor.close()
return image
}
This returns a bitmap, which I then pass into ImageView.bitmapFromUri. Unfortunately, this produces the same error, this time coming from contentResolver.openFileDescriptor(uri, "r")
when I fetch the URI from the database
That's not an option with ACTION_PICK. You cannot reliably persist a Uri that you received from ACTION_PICK and use it again later. Your access rights to the content are short-lived.
If you switch to ACTION_OPEN_DOCUMENT and use takePersistableUriPermissions() on a ContentResolver when you get the Uri in onActivityResult(), you can safely persist that Uri. You might still run into problems using it later — for example, the user could delete the image that the Uri points to — but you will not encounter the access problem that you are seeing here.
Related
Having invoked a directory selector on Android with:
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
)
activity?.startActivityForResult(intent, REQUEST_CODE_FOLDER_PERMISSION)
And having obtained the URI of said route in onActivityResult(), being the URI of the form (example in case of having chosen a folder named backup in the root of the external storage):
content://com.android.externalstorage.documents/tree/primary:backup
At this point, how do you write a file to that location? After researching various answers on how to write files using the Media Store API, all the examples I've seen use constants to refer to already existing media directories, but in my case I want to create a new document (which is a JSON file) in the directory chosen by the user.
You will not use the MediaStore to save a file if you obtained an uri using ACTION_OPEN_DOCUMENT_TREE to get permission for a folder.
Just continue to use Storage Access Framework and implement DocumentFile.createFile().
Pretty basic exercise for learning SAF.
If you want to use the MediaStore to save a file then you do not have the user to select a folder first.
Thanks to #CommonsWare for pointing me in the right direction:
var outputStream: OutputStream? = null
try {
val uri = Uri.parse(path)
val document = DocumentFile.fromTreeUri(context, uri)
val file = document?.createFile(mimeType, filename)
?: throw Exception("Created file is null, cannot continue")
val fileUri = file.uri
val contentResolver = context.contentResolver
outputStream = contentResolver.openOutputStream(fileUri)
val bytes = content.toByteArray()
outputStream?.write(bytes)
outputStream?.flush()
} catch (e: Exception) {
// Handle error
} finally {
outputStream?.close()
}
I am using an action chooser intent to ask the user to choose one of the following from a fragment:
MediaStore.ACTION_IMAGE_CAPTURE
MediaStore.ACTION_VIDEO_CAPTURE
Intent.ACTION_GET_CONTENT
I want to be able to distinguish the selected action of the user because I have different functions per action.
Below is my current code.
private val intentLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
//Identify the intent selected
//TODO: image from camera
//TODO: video from camera
//TODO: any file
}
}
private fun dispatchActionChooserIntent() {
Intent(Intent.ACTION_CHOOSER).also { actionChooserIntent ->
val cameraIntent = createCameraIntent(MediaStore.ACTION_IMAGE_CAPTURE)
val videoIntent = createCameraIntent(MediaStore.ACTION_VIDEO_CAPTURE)
val filePickerIntent = createFilePickerIntent()
actionChooserIntent.putExtra(Intent.EXTRA_INTENT, filePickerIntent);
actionChooserIntent.putExtra(
Intent.EXTRA_INITIAL_INTENTS,
arrayOf<Intent>(cameraIntent, videoIntent)
);
cameraIntent.putExtra("intentAction",Intent.ACTION_CHOOSER)
actionChooserIntent.putExtra(Intent.EXTRA_TITLE, "")
}
}
private fun createFilePickerIntent(fileType: String = "*/*"): Intent {
return Intent(Intent.ACTION_GET_CONTENT).also { filePickerIntent ->
filePickerIntent.type = fileType
filePickerIntent.addCategory(Intent.CATEGORY_OPENABLE)
filePickerIntent.resolveActivity(
(activity as AppCompatActivity).applicationContext.packageManager)
}
}
private fun createCameraIntent(cameraAction: String): Intent {
return Intent(cameraAction).also { cameraIntent ->
// Ensure that there's a camera activity to handle the intent
cameraIntent.resolveActivity(
(activity as AppCompatActivity).applicationContext.packageManager)
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, cameraIntentURI)
}
}
the result only includes the resultCode and the data
Sample result from taking a photo
Sample result from taking a video
Sample result from choosing a file
If the received uri is null the camera was choosen.
The MediaStore.ACTION_VIDEO_CAPTURE honors Intent.EXTRA_OUTPUT and you will use your fileprovider there.
In contrary to the camera action you will get the supplied uri back. (Google has learned something).
So if you receive an uri in onActivityResult you can check for uri.getAuthority().
if the authority is the same as your fileprovider you know it is from video capture.
There can be two solutions.
Android way
When you receive the URI as data as a result. Process that URI and try to get the mime type. Based on mime type you can understand what type of data you have received. Check how it can be done here https://developer.android.com/training/secure-file-sharing/retrieve-info
Logical way
Convert the Uri into an instance of type File. Get the extension of a file and check what type of extension it is.
jpeg, png, etc fall into the image category, mp4 kind of extension is a video and the rest can be any other file. Try this to get the file extension https://www.tutorialkart.com/kotlin/kotlin-get-file-extension/#:~:text=Get%20File%20Extension%20in%20Kotlin,extension%20is%20a%20String%20value.
I would recommend exploring option 1. That's the right and safe approach.
Good luck!
binding.btnShareOne.setOnClickListener {
val intent = Intent(Intent.ACTION_SEND).setType("image/*")
val bitmapDrawable = binding.ivLayoutOne.drawable.toBitmap()
val path = MediaStore.Images.Media.insertImage(
contentResolver,
bitmapDrawable,
"tempimage",
null
)
val uri = Uri.parse(path)
intent.putExtra(Intent.EXTRA_STREAM, uri)
startActivity(Intent.createChooser(intent, "Share Image"))
}
This is my code. I want to get the path from the bitmap and pass it to Uri. When I run my current code it gives me " java.lang.NullPointerException: uriString " error.
I have researched a bit and I think it will be solved by scoped storage but I cannot seem to implement it.
Since .insertImage() is deprecated and also Uri is returning null, this method of mine is not working to get image from image view and share it.
Please help.
A bitmap object doesn't have a path. A Bitmap object is an in memory object. Its just raw bytes. Even if it came from a file, that association isn't kept around.
My goal is to:
Save media file to External Storage (in my case it's photo).
Get file path or URI of saved data.
Save it to SQLite (either file path or content URI or smth else).
Be able to get correct URI to this content at any point in the future.
It's very similar to what other very popular application do - they create their directory in 'Pictures' folder and store there photos and use them in their applications while they're also available for viewing using gallery/file explorer etc.
As I understand recommended way to save media content (image, f.e.) is to use MediaStore API and as a result I get content URI, which I can use later.
But then I read that these content URIs might be changed after re-scan of Media happens, so it looks it's not reliable. (For example if SD card is used and it's taken out and inserted again)
At the same time usage of absolute file paths is not recommended and there's tendency to deprecate APIs which use absolute file paths to work with External Storage. So it doesn't look reliable either.
I can only imagine the following solution:
Use unique auto-generated file name while saving (like UUID).
When I need to get content URI (f.e. want to render photo in ImageView) - I can use ContentResolver and search for content URI using file name filter.
Problem with this approach is that I have a lot of photos (gallery) and querying it using ContentResolver can affect performance significantly.
I feel like I'm over complicating things and missing something.
You are indeed overcomplicating things.
Store file to the needed folder in the filesystem(it is better to name the folder under your app name)
Store this path or URI path - whatever you like. (Do not hardcode passes though in your app - device vendors may have different base paths in their devices)
As long as the folder is named the same and files in it named the same(as in your db) - you will be able to access them even if the sdcard was taken out and then put back in.
There are possible complications after reindexing - but for the eight years I work as Android dev I encountered it only once, thus you can easily ignore this stuff.
If you want to have more control over what you store and want to limit access to it - store data into the inner storage of your app - this way you will be 100% sure of where the data is and that it is not tampered with.
Starting from Android 10 you have scoped storage - it is like internal storage but it may be even on an external sdcard.
Here is a small overview of possible storage locations.
And don't overthink it too much - it is a default usecase of the phone and it works just as you would expect - pretty ok and pretty stable.
first, you have to apply for external storage permission in manifest and Runtime Permission Also.
after creating a directory for saving an image in this directory.
you have to also add file provider in XML and code side because it's required.
now it's time to code check my code for saving an image in the folder also these image in the gallery and get the path from a file path.
convert URI to bitmap
http://prntscr.com/10dpvjj
save image function from getting bitmap
private String save(Bitmap bitmap) {
File save_path = null;
if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) {
try {
File sdCard = Environment.getExternalStorageDirectory();
File dir = new File(sdCard.getAbsolutePath() + "/SaveDirectory");
dir.mkdirs();
File file = new File(dir, "DirName_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(Calendar.getInstance().getTime()) + ".png");
save_path = file;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
FileOutputStream f = null;
f = new FileOutputStream(file);
MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, null, null);
if (f != null) {
f.write(baos.toByteArray());
f.flush();
f.close();
}
} catch (Exception e) {
// TODO: handle exception
}
Share(save_path); // call your Function Store into database
Log.e("PathOFExec----", "save: " + save_path);
}
get store image location into your database if you wish
private void Share(File savePath) {
if (savePath != null) {
Uri uri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", savePath);
Intent share = new Intent(Intent.ACTION_SEND);
share.setType("image/*");
share.putExtra(Intent.EXTRA_TEXT, "TextDetail");
share.putExtra(Intent.EXTRA_STREAM, uri);
context.startActivity(Intent.createChooser(share, "Share Image!"));
//after getting URI you can store the image into SQLite databse for get uri
}
}
I would recommend using Intent.ACTION_OPEN_DOCUMENT for your demand.
1. Create Photo Picking Intent:
val REQUEST_CODE_PICK_PHOTO = 1
fun pickAndSavePhoto(requestCode: Int) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.type = "image/*"
startActivityForResult(intent, requestCode)
}
2. Handle Result:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_PICK_PHOTO && resultCode == RESULT_OK) {
val imageUri = data!!.data!!
//save this uri to your database as String -> imageUri.toString()
}
}
3. Get Image back and Display on ImageView:
fun getBitmapFromUri(context: Context, imageUri: Uri): Bitmap? { //uri is just an address, image may be deleted any time, if so returns null
val bitmap: Bitmap
return try {
val inputStream = context.contentResolver.openInputStream(imageUri)
inputStream.use {
bitmap = BitmapFactory.decodeStream(it)
}
bitmap
} catch (e: Exception) {
Log.e("getBitmapFromUri()", "Image not found.")
null
}
}
val bitmap = getBitmapFromUri(context, imageUri) //get uri String from database and convert it to uri -> uriString.toUri()
if (bitmap != null) {
imageView.setImageBitmap(bitmap)
}
Only ACTION_OPEN_DOCUMENT can access file uri permanently:
Android Retrieve Image by Intent Uri Failed: "has no access to content..."
Demo: https://www.youtube.com/watch?v=LFfWnt77au8
I want to take picture using the MediaStore.ACTION_IMAGE_CAPTURE intent, and then save it to a specific path with contentresolver.insert() and passed the uri with EXTRA_OUTPUT. Here is how I do it(with permission WRITE_EXTERNAL_STORAGE granted):
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
val imageData = ContentValues()
val imageTableUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
imageData.put(MediaStore.MediaColumns.DISPLAY_NAME, "${filename}.jpg")
imageData.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
imageData.put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/MyPath")
val imageUri = contentResolver.insert(imageTableUri, imageData)
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
intent.also { takePictureIntent ->
takePictureIntent.resolveActivity(packageManager).also {
startActivityForResult(takePictureIntent, takePictureRequestCode)
}
}
This code will work. But if I:
Cancel the camera intent by the Back button, or
Switch to other apps while staying in the camera invoked by my activity
the Photo app will show a incomplete photo, whose information cannot be shown:
incomplete photo in Photo app(indicated by the red square)
Furthermore, the same DISPLAY_NAME will cause contentResolver.insert() to return null even if I have manually deleted that incomplete photo(with Photo app or File Manager), and I also cannot find the file by contentResolver.query() with the clause selecting that file name.
For the first case, I may invoke contentResolver.delete() in onActivityResult() method of my activity since the uri may be kept. But is there a way to handle the second case(which may sometimes cause the application end directly)? Or did I misuse one of contentResolver or the camera intent?