Android SharedPreferences Tutorial and Example

Picking photos from the gallery with an intent in Android

Image selection is one of those small features that quietly shapes how a mobile app feels in everyday use. Australians lean heavily on Android handsets, with manufacturers like Samsung, Google and Oppo dominating sales in Sydney, Brisbane and Perth showrooms, so the gallery picker has to feel native rather than bolted on. Whether you are building a profile upload screen for a Melbourne-based startup or a photo journal for a travel blogger exploring the Whitsundays, getting the intent-driven flow right is essential.

The same gallery plumbing shows up across more specialised verticals too. Fintech and gaming services operating in the Australian market, including platforms that accept a casino debit card, often request photo access to verify identity documents or proof-of-deposit slips before processing payments. Understanding how to fire an intent, capture the URI, and convert it into something renderable is therefore a skill that pays off well beyond social apps.

Understanding the intent-based gallery picker

Android exposes gallery selection through a system of implicit intents. Rather than pointing at a specific class, the developer declares an action such as ACTION_PICK or ACTION_GET_CONTENT and lets the system resolve a handler. The user can then choose between the stock Gallery, Google Photos, or any third-party file manager registered for the relevant image/* MIME filter.

Two actions tend to dominate legacy codebases. ACTION_PICK works against a specific content provider such as MediaStore.Images, while ACTION_GET_CONTENT is more flexible and accepts documents from the Storage Access Framework. The newer Photo Picker, introduced in Android 13 and backported via Google Play system updates, abstracts both paths and removes the need for storage permissions in most cases.

Adding the required permissions and FileProvider setup

For pre-Android-13 devices using ACTION_PICK, you still need to declare READ_EXTERNAL_STORAGE in the manifest, with a runtime request on API 23 and above. The Photo Picker sidesteps this entirely by running inside its own process and handing back a temporary read grant, but if you are supporting a wider install base across regional Australian carriers, plan for both paths.

When the returned URI needs to be shared with another component, a FileProvider keeps the SecurityException warnings at bay. Register it in the manifest with the standard androidx.core.content.FileProvider authority, drop an xml/file_paths.xml resource into res/xml/, and reference that authority when constructing the URI. A typical configuration exposes a cache and an external-files subdirectory, which is enough for most camera-and-gallery combinations.

Launching the picker using the traditional intent

The classic launch is short enough to memorise. Build an Intent with the chosen action, set its type to image/*, optionally pass an EXTRA_ALLOW_MULTIPLE flag, then call startActivityForResult. On modern code, the result is delivered to an ActivityResultLauncher registered through registerForActivityResult, which is cleaner than overriding onActivityResult.

val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
    type = "image/*"
    addCategory(Intent.CATEGORY_OPENABLE)
}
launcher.launch(intent)

Always set CATEGORY_OPENABLE so the resolver excludes content providers that cannot return a stream you can read, such as live camera sources. Australian users tend to install a wide mix of gallery apps, so a tighter filter keeps the chooser from ballooning into an unwieldy list.

Modern approach with ActivityResultContracts

The AndroidX team has steadily replaced the old callback pair with contracts, and PickVisualMedia is the recommended contract for single images. It is part of the androidx.activity library and works all the way back to API 19, which covers virtually every handset sold in Adelaide or Darwin over the last decade.

val pickMedia = registerForActivityResult(
    ActivityResultContracts.PickVisualMedia()
) { uri ->
    if (uri != null) handleSelectedImage(uri)
}

pickMedia.launch(
    PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)

Behind the scenes, the contract routes the request through the Photo Picker when available and falls back to ACTION_GET_CONTENT on older releases. The contract also handles the temporary URI permission grant, so the receiver activity can read the bitmap without an explicit grantUriPermission call.

Handling the returned URI and reading the image

Once a URI arrives, the safest way to read it is through a ContentResolver. Calling openInputStream(uri) gives you a raw InputStream that you can decode with BitmapFactory.decodeStream or hand to an image-loading library such as Glide or Coil. For larger sources, downsample on the way in by reading MediaStore.Images.ImageColumns.WIDTH and HEIGHT first, then passing an inSampleSize to the decoder.

Caching the path matters for offline flows. A common approach is to copy the stream into the app's cacheDir and persist the resulting file name, which avoids hitting a stale URI grant the next time the user reopens the activity. This pattern is especially useful in apps that let users revisit uploaded content offline, such as travel guides caching Whitsunday reef shots before a ferry ride.

Comparing traditional intent and Photo Picker

Feature Traditional intent (ACTION_GET_CONTENT) Photo Picker contract
Minimum API Works back to API 1 Back to API 19 via AndroidX
Storage permission Required on API 32 and below Not required on most devices
Multi-select support Via EXTRA_ALLOW_MULTIPLE First-class via PickMultipleVisualMedia
Picker UI System chooser, varies by app Unified Material UI
URI lifetime Can be revoked after process death Stable for the lifetime of the request

The table makes the trade-off obvious. Legacy intents still win when you must support obscure devices or specific provider URIs, while the Photo Picker contract is the default for any new Australian app hitting the Play Store in 2024 and beyond.

Common pitfalls when handling image selection

A few issues bite developers repeatedly. The first is rotation: loading a full-resolution JPEG straight into an ImageView on the main thread will trip the dreaded OutOfMemoryError on budget devices common in the prepaid handset market. The second is failing to handle null from the launcher, which happens whenever the user dismisses the chooser without picking anything.

URI persistence is the third trap. If the activity is recreated while the result is in flight, the launcher survives but the URI permission does not, so caching the bitmap or copying the file early is wise. Finally, watch out for SecurityException on devices that ship a stricter scoped storage model; catching it and falling back to a manual READ_MEDIA_IMAGES request keeps the user experience intact rather than crashing the screen.