Selecting documents in an Android app
Using file chooser and file picker dialogs in an Android application usually means delegating selection to the operating system. Rather than build a dialog that lists every file, the app sends an intent and Android brings together local storage, Downloads and installed cloud providers. The chosen item comes back as a content URI, not necessarily as a filesystem path.
The right implementation depends on whether access is needed once or across app restarts, which document types are accepted, and which Android versions are supported. The modern AndroidX approach uses the Activity Result API, returns a URI, and keeps provider-specific details outside the rest of the app.
How Android file selection works
A file picker is a system-provided chooser, even when a device maker presents it as a bottom sheet instead of a traditional dialog. When an app launches the relevant contract, Android may display the Files app, Downloads, Google Drive, Dropbox or another provider that exposes documents. Availability varies by device, account and system version, so the app should describe the required type rather than promise one provider.
A content:// URI identifies the resource through a content provider. It can represent a PDF on a phone, an image backed by cloud storage, or a document exposed by an enterprise service. This indirection explains why File(uri.path) is unsafe: the URI path is not a normal filesystem location.
Australian use cases make the distinction practical. A rates notice downloaded from the Brisbane City Council site and a school file saved in the cloud can reach the same screen. The app accepts the document without checking where it physically resides.
Launching the picker from Kotlin
Register the contract as a field in the Activity or Fragment so Android manages its lifecycle. OpenDocument is the usual choice for reading files and can retain a URI grant when the provider supports it. MIME filters narrow the chooser without excluding files held by third-party services.
private var selectedUri: Uri? = null
private val openDocument =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri?.let {
selectedUri = it
showSelectedFile(it)
}
}
OpenMultipleDocuments returns a list when several files are needed, while */* accepts every supported type. Trigger the launch from a user action; opening it automatically can look intrusive and may be restricted on recent Android releases.
findViewById<Button>(R.id.choose_button).setOnClickListener {
openDocument.launch(arrayOf("application/pdf", "image/*"))
}
Reading and retaining the selected file
Do not request broad storage permission simply to open a document selected through ACTION_OPEN_DOCUMENT. The user grants temporary access to that URI, and the app may retain a narrower read grant. A provider can refuse durable access, so persistence is an enhancement rather than a guarantee.
private fun keepReadAccess(uri: Uri) {
try {
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} catch (_: SecurityException) {
// Continue with the temporary access already granted.
}
}
Read through contentResolver.openInputStream(uri) and close the stream promptly. Copy it into app storage only when the feature needs a stable local file. Run URI resolution or copying in a coroutine so a large document or slow cloud provider does not block the main thread.
Query OpenableColumns.DISPLAY_NAME and OpenableColumns.SIZE for metadata, although remote providers may return a null size or delayed name. Display what is available, fall back to “Selected document”, and avoid assuming that an extension matches the MIME type.
Showing progress without losing context
After the picker closes, the app may still need time to query metadata, download a preview or copy the file. Show progress during that work and retain the chosen filename in the interface. The system picker runs in another app, so an indefinite spinner shown before launch is rarely useful.
A ProgressBar, the filename and a short status message form a useful visual state. The spinner background colour example shows how to keep a spinner visible against a form or card. Match it to the theme and avoid using colour as the only signal.
Treat failures as normal states. SecurityException can indicate a lost grant, while FileNotFoundException may mean that a cloud item moved or was deleted. If access expired, ask the user to choose it again. Friendly Australian copy often works well in phrases such as “Hang on while we open that file”, reflecting everyday conversational speech without becoming vague.
Supporting cloud files and Australian users
Cloud providers are document sources too, but they do not behave exactly like local storage. Thumbnails can arrive slowly, names may be normalised, and opening can require an account or network connection. A resilient app uses ContentResolver APIs and avoids assumptions about extensions or physical paths.
Google Play is the main Android storefront in Australia, and Galaxy devices sit alongside lower-cost handsets used in metropolitan and regional areas. Testing is strongest when it covers a current Samsung phone, a small-screen handset and an older supported release. Storage providers, screen sizes and permission implementations matter more than one flagship emulator.
Small businesses and field teams commonly move between shared drives, Dropbox, Google Drive and local Downloads folders. An Adelaide contractor might attach a quote from a shared drive and save a receipt photo for later. Preserving the URI grant, handling offline periods and keeping the filename visible supports those mixed workflows.
Comparing picker approaches
OpenDocument offers MIME filtering and can retain read access across restarts. GetContent suits one-off content, but its URI grant is temporary. ACTION_OPEN_DOCUMENT is the platform intent behind the modern contract; ACTION_GET_CONTENT remains useful for immediate access with less ceremony.
Teams choosing a long-term policy can compare these contracts with any external document handling guidance and their own security requirements.
| Approach | Best fit | Access behaviour | Important caution |
|---|---|---|---|
OpenDocument contract |
Documents that may be reopened | Read grant can be persisted | Provider must support durable grants |
GetContent contract |
One-time import or shared content | Usually temporary | Do not rely on access after restart |
ACTION_OPEN_DOCUMENT |
Direct control of the platform intent | Can retain selected-file access | Handle result flags and lifecycle manually |
ACTION_GET_CONTENT |
Simple chooser with broad provider support | Temporary | Unsuitable for long-term references |
For an app that opens documents or stores references to them, OpenDocument is the clearest default. A share or one-time import can use GetContent. In every case, narrow URI access, reading through ContentResolver, and tolerance for incomplete metadata produce a dependable Android file-selection flow.