[ACTION REQUIRED] Ensure Your App Complies with Google Play’s Latest Policy by January 22 2025
Hello fellow Android enthusiasts! The new year has just begun and it’s already time to tackle the first significant Google Play policy requirement of the year. Feels great doesn’t it?
You probably already know what I’m talking about. Yes that email from Google about the upcoming changes. You’ve received it haven’t you?
Perhaps you’ve read it sighed and set it aside for “later”.
If you’ve seen that email give this article a clap so I know I’m not alone in this.
Now let’s get to it. What are your options? Well unless your app is a gallery or heavily depends on image or camera-related functionalities you don’t have much choice but to remove the READ_MEDIA_IMAGES
permission from your manifest.
This article specifically tackles the READ_MEDIA_IMAGES
permission scenario and focuses on handling image permissions only. It uses a DialogFragment
in the view system that allows users to either capture a photo with the camera or choose one from the gallery. While this implementation is based on the view system the underlying concepts are equally valid for Jetpack Compose. Even though Jetpack Compose does not use DialogFragment
the rest of the code—such as handling permissions launching the camera and interacting with the system photo picker—can be directly reused or easily adapted for Compose-based implementations. If you’d like me to address the video permission changes please let me know in the comments!
The Problem
With Android 13 and its updated permissions policy apps targeting API level 33 must adhere to scoped storage and avoid requesting broad permissions like READ_MEDIA_IMAGES
unless absolutely necessary. While these permissions were handy in the past they pose potential privacy risks which is why Google is tightening their usage.
If your app doesn’t need unrestricted access to all images you must shift to alternatives like the system photo picker or scoped storage. Let’s walk through a practical implementation.
The Solution
Below is a sample implementation to handle image selection without using the READ_MEDIA_IMAGES
permission. The implementation demonstrates a DialogFragment
that provides users with two options: shooting a photo using the camera or selecting an image from the gallery. While the example is written in the context of the view system the same principles can be applied to Jetpack Compose for similar functionality. You can implement comparable UI behavior in Jetpack Compose by using its dialog and permissions APIs while reusing the logic for handling camera and gallery interactions. This includes both capturing images via the camera and selecting them from the system photo picker.
Code Implementation
/** * DialogFragment for selecting an image by either capturing it using the camera * or picking an existing one from the gallery. Ensures compliance with scoped storage. */ class FileSelectionDialog : DialogFragment() { private lateinit var cameraLauncher: ActivityResultLauncher<Intent> private lateinit var galleryLauncher: ActivityResultLauncher<Intent> private lateinit var cameraPermissionLauncher: ActivityResultLauncher<String> private var currentPhotoPath: String = "" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) cameraPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> if (granted) { launchCameraForImage() } else { Toast.makeText(requireContext(), "Camera permission denied", Toast.LENGTH_SHORT).show() } } cameraLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { handleCameraResult() } } galleryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK && result.data != null) { handleGalleryResult(result.data!!) } } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.dialog_file_selection, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) view.findViewById<Button>(R.id.buttonCaptureImage).setOnClickListener { captureImage() } view.findViewById<Button>(R.id.buttonSelectImage).setOnClickListener { openMediaPicker() } } /** * Requests camera permission or launches the camera if already granted. */ private fun captureImage() { val cameraPermission = Manifest.permission.CAMERA if (ContextCompat.checkSelfPermission(requireContext(), cameraPermission) != PackageManager.PERMISSION_GRANTED ) { cameraPermissionLauncher.launch(cameraPermission) } else { launchCameraForImage() } } /** * Launches the camera intent to capture an image and saves it to a file. * Uses FileProvider for secure URI access. */ private fun launchCameraForImage() { val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) if (takePictureIntent.resolveActivity(requireActivity().packageManager) != null) { val photoFile = createImageFile() if (photoFile != null) { val photoUri: Uri = FileProvider.getUriForFile( requireContext(), "com.medium.image.permission.fileprovider", photoFile ) takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri) takePictureIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) cameraLauncher.launch(takePictureIntent) } } } /** * Creates a temporary file for storing the captured image. * Returns the created file or null in case of an error. * * @return File object representing the created image file. */ private fun createImageFile(): File? { return try { val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) val storageDir = requireActivity().getExternalFilesDir(Environment.DIRECTORY_PICTURES) File.createTempFile("JPEG_$timeStamp", ".jpg", storageDir).apply { currentPhotoPath = absolutePath } } catch (ex: IOException) { Toast.makeText(requireContext(), "Error creating file", Toast.LENGTH_SHORT).show() null } } /** * Opens the system media picker to select an image from the gallery. * Uses ACTION_OPEN_DOCUMENT for better compatibility with Scoped Storage. */ private fun openMediaPicker() { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "image/*" } galleryLauncher.launch(intent) } /** * Handles the result of a camera capture. * Decodes the image file and confirms successful capture. */ private fun handleCameraResult() { val bitmap = BitmapFactory.decodeFile(currentPhotoPath) if (bitmap != null) { Toast.makeText(requireContext(), "Image captured successfully!", Toast.LENGTH_SHORT).show() } else { Toast.makeText(requireContext(), "Failed to capture image", Toast.LENGTH_SHORT).show() } } /** * Handles the result of selecting an image from the gallery. * Retrieves the image URI and confirms selection. * * @param data The intent containing the selected image data. */ private fun handleGalleryResult(data: Intent) { val imageUri: Uri? = data.data Toast.makeText(requireContext(), "Image selected successfully!", Toast.LENGTH_SHORT).show() } companion object { /** * Constant value for camera permission request. */ private const val CAMERA_PERMISSION_REQUEST_CODE = 101 } }
Here is a visual demo of the above code:
Job Offers
Conclusion
By leveraging the system photo picker and ensuring the camera captures images directly to a file, you can remove the READ_MEDIA_IMAGES
permission from your app and comply with Google Play’s latest requirements. This not only keeps your app on the Play Store but also improves user privacy and security.
Let me know if you’d like me to cover the changes for video permissions in a future article!

Dobri Kostadinov
Android Consultant | Trainer
Email me | Follow me on LinkedIn | Follow me on Medium | Buy me a coffee
This article is previously published on proandroiddev.com.