Accessing Images On Android Without Storage PermissionšŸ˜

Abdul Rehman
6 min readDec 6, 2019
Android 10 policy changes.

With the release of new android 10 there are many changes made in favor of user privacy. One of those changes is Scoped Storage Access!. It has made lot of developer frustrated as now we cannot access the images from device storage directly.

Even for doing simple task like updating profile picture is very cumbersome. So in this post I will help those who are facing the same issue.

So in this post we will see how to select image from device storage or capture image using camera and crop the image before uploading it to our server. In simple term we will fetch the image and get its File() reference.

Let's start by selecting image from device itself.

fun selectImage(activity: Activity) {
val intent = Intent()
// Show only images, no videos or anything else
intent.type = "image/*"
intent.action = Intent.ACTION_GET_CONTENT
activity.startActivityForResult(intent, IMAGE_CHOOSER)
}

Use the above method where you want to fetch the image. Then in activity result you will get the reference to the selected image Uri.

fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent ? ) { if (resultCode != RESULT_OK) { return } if (requestCode == IMAGE_CHOOSER &&
data != null &&
data.getData() != null) {
//We cannot access this Uri directly in android 10
selectedImageUri = data.getData()
}
super.onActivityResult(requestCode, resultCode, data)
}

Now we will convert the Uri that we received into Bitmap. We can use Bitmap directly, but we cannot use Uri due to android 10 changes.

fun getBitmap(context: Context, imageUri: Uri): Bitmap? {
return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {

ImageDecoder.decodeBitmap(
ImageDecoder.createSource(
context.contentResolver,
imageUri))

} else {

context
.contentResolver
.openInputStream(imageUri)?.use { inputStream ->
BitmapFactory.decodeStream(inputStream)
}

}
}
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent ? ) {
if (resultCode != RESULT_OK) { return }

if (requestCode == IMAGE_CHOOSER &&
data != null &&
data.getData() != null) {

//We cannot access this Uri directly in android 10
selectedImageUri = data.getData()

//Later we will use this bitmap to create the File.
val selectedBitmap: Bitmap = getBitmap(this, selectedImageUri)

}
super.onActivityResult(requestCode, resultCode, data)
}

Now at the end we will store the Bitmap at different location where we can directly access it.

fun convertBitmapToFile(destinationFile: File, bitmap: Bitmap) {
//create a file to write bitmap data
destinationFile.createNewFile()
//Convert bitmap to byte array
val bos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, bos)
val bitmapData = bos.toByteArray()
//write the bytes in file
val fos = FileOutputStream(destinationFile)
fos.write(bitmapData)
fos.flush()
fos.close()
}
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent ? ) {
if (resultCode != RESULT_OK) { return }

if (requestCode == IMAGE_CHOOSER &&
data != null &&
data.getData() != null) {

//We cannot access this Uri directly in android 10
selectedImageUri = data.getData()

//Later we will use this bitmap to create the File.
val selectedBitmap: Bitmap = getBitmap(this, selectedImageUri)

/*We can access getExternalFileDir() without asking any storage permission.*/
val selectedImgFile = File(
getExternalFilesDir(Environment.DIRECTORY_PICTURES),
getTimestamp().toString() + "_selectedImg.jpg")

convertBitmapToFile(selectedImgFile, selectedBitmap)
}
super.onActivityResult(requestCode, resultCode, data)
}

Note: Execute convertBitmapToFile() method in background for better performance. I use Coroutine to execute it in background.

fun convertBitmapToFile(destinationFile: File, bitmap: Bitmap) {    CoroutineScope(Dispatchers.IO).launch {        //create a file to write bitmap data
destinationFile.createNewFile()
//Convert bitmap to byte array
val bos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, bos)
val bitmapData = bos.toByteArray()
//write the bytes in file
val fos = FileOutputStream(destinationFile)
fos.write(bitmapData)
fos.flush()
fos.close()
}
}

The selectedImgFile is the reference to the File() which we can directly use to do any type of processing on the Image. We will crop the image and upload it to our server. You can use this File() to do you desired task.

Note: Here uCrop library is used for cropping image.

fun startCrop(context: Activity, sourceUri: Uri, destinationUri: Uri) {
val options = UCrop.Options()
options.setHideBottomControls(true)
options.withMaxResultSize(640,640)
UCrop.of(sourceUri, destinationUri)
.withAspectRatio(1f, 1f)
.withOptions(options)
.start(context as AppCompatActivity)
}
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent ? ) {
if (resultCode != RESULT_OK) { return }

if (requestCode == IMAGE_CHOOSER &&
data != null &&
data.getData() != null) {

//We cannot access this Uri directly in android 10
selectedImageUri = data.getData()

//Later we will use this bitmap to create the File.
val selectedBitmap: Bitmap = getBitmap(this, selectedImageUri)

/*We can access getExternalFileDir() without asking any storage permission.*/
val selectedImgFile = File(
getExternalFilesDir(Environment.DIRECTORY_PICTURES),
getTimestamp().toString() + "_selectedImg.jpg")

convertBitmapToFile(selectedImgFile, selectedBitmap)

/*We have to again create a new file where we will save the cropped image. */
val croppedImgFile = File(
getExternalFilesDir(Environment.DIRECTORY_PICTURES),
getTimestamp().toString() + "_croppedImg.jpg")

startCrop(
this,
Uri.fromFile(selectedImgFile),
Uri.fromFile(croppedImgFile))

} else if (requestCode == UCrop.REQUEST_CROP) {

try {

/*After the cropping is done we will get the cropped image Uri here. We can use this Uri and create file and use it for other purpose like saving to cloude etc.*/
croppedImageUri = UCrop.getOutput(data)
printLog("path...\${croppedImageUri!!.path}")
val bitmap: Bitmap = MediaStore.Images.Media.getBitmap(
getContentResolver(), croppedImageUri)

profile_pic.setImageBitmap(bitmap)

//Uploading the image to cloud.
uploadToAmazon(File(croppedImageUri.path), "profile.jpg")

} catch (e: Exception) { e.printStackTrace()}

} else if (resultCode == UCrop.RESULT_ERROR) {

val cropError = UCrop.getError(data)
printLog(cropError!!.message)

}
super.onActivityResult(requestCode, resultCode, data)
}

So we are now able access the image stored in device and process it without asking any additional storage permission. Hurrah!

Extra: If you want to access the image using camera app check out below code.

//use this method to open camera. Before calling this method make sure you have access to camera permission. If not first ask camera permission from user. fun takePicture(context: Activity, imageName: String) {try {
val capturedImgFile = File(
context.getExternalFilesDir(Environment.DIRECTORY_PICTURES),
imageName)

captureImgUri = FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".my.package.name.provider",
capturedImgFile)
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).also {

it.putExtra(MediaStore.EXTRA_OUTPUT, captureImgUri)
it.putExtra("return-data", true)
it.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.startActivityForResult(it, REQUEST_CODE_TAKE_PICTURE)

}
} catch (e: ActivityNotFoundException) {e.printStackTrace()}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {

if (resultCode != RESULT_OK) {return}

if (requestCode == REQUEST_CODE_TAKE_PICTURE) {

/*We cannot access the image directly so we again create a new File at different location and use it for futher processing.*/

Bitmap capturedBitmap = getBitmap(this, captureImgUri)

/*We are storing the above bitmap at different location where we can access it.*/
val capturedImgFile = File(
getExternalFilesDir(Environment.DIRECTORY_PICTURES),
getTimestamp() + "_capturedImg.jpg");
convertBitmapToFile(capturedImgFile, capturedBitmap)/*We have to again create a new file where we will save the processed image.*/ val croppedImgFile = File(
getExternalFilesDir(Environment.DIRECTORY_PICTURES),
getTimestamp() + "_croppedImg.jpg");
startCrop(
this,
Uri.fromFile(capturedImgFile),
Uri.fromFile(croppedImgFile))
}
super.onActivityResult(requestCode, resultCode, data)
}

Note: While opening camera app we have used FileProvider to get the reference to Uri. Android does not allow to access the captured image Uri directly, so we have to use FileProvider.

FileProvider.getUriForFile(context, context.applicationContext.packageName + ā€œ.my.package.name.providerā€, capturedImgFile)

If you donā€™t know how to use FileProvider go through below code.

Step 1. Add a class extending FileProvider.

public class GenericFileProvider extends FileProvider {}

Step 2. Add a FileProvider <provider> tag in AndroidManifest.xml under <application> tag. Specify a unique authority for the android:authorities attribute to avoid conflicts, imported dependencies might specify ${applicationId}.provider and other commonly used authorities.You can use any name but use the same name at the time of invocation.

Like here we have defined ā€œ${applicationId}.my.package.name.providerā€ so when accessing URI we have to use the same name.

FileProvider.getUriForFile(this, getApplicationContext().getPackageName() + ā€œ.my.package.name.providerā€, capturedImgFile);

<provider 
android:name="com.threembed.utilities.GenericFileProvider" android:authorities="${applicationId}.my.package.name.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>

Step 3. Then create a provider_paths.xml file in res/xml folder. Folder may be needed to created if it doesnā€™t exist. The content of the file is shown below. It describes that we would like to share access to the External Storage at root folder (path=ā€.ā€) with the name external_files.

<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external_files"
path="." />
</paths>

Step 4. Add the permission to intent while opening camera.

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

Step 5. Thatā€™s it you are done. Now you can call the Uri as below.

captureImgUri = FileProvider.getUriForFile(this, getApplicationContext().getPackageName() + ".my.package.name.provider", capturedImgFile);

--

--

Abdul Rehman

Mobile developer in love with Flutter and Android ā™„ļø