Recommended resources
The freely available resources this lesson will direct you to.
Hello! Last lesson, you separated two questions that are easy to conflate: Can Android launch an Activity for this Intent? and Which handlers can my app discover? This lesson turns those questions into observations. You will inspect component declarations, query matching Activities from Kotlin, and use adb to compare what is installed with what actually launches.
The aim is a repeatable diagnostic method, not a preflight check before every startActivity() call. Package inspection tells you what is visible at a particular moment; launching remains a separate operation.
Three views of the same device
Suppose an implicit ACTION_SEND Intent with MIME type text/plain opens a share target. There are at least three useful views of that event:
- Package inventory: Which packages and manifest-declared components are installed?
- Intent query: Which Activities does a particular caller discover as matches?
- Launch: What happens when the system processes the Intent now?
These views need not produce identical-looking results. A package can be installed without declaring a matching Activity. An Activity can be declared but disabled, inaccessible to another app, or missing the required filter. On Android 11 and later, your app’s package-visibility rules can also limit its discovery results. Finally, resolution can involve user defaults or a resolver UI; a list of candidates is not a record of what the user selected.
Android Developers’ Intents and intent filters describes PackageManager queries as inspection rather than activation. Read its Intent matching subsection for the distinction between methods that list candidates and methods that resolve one.
Intents and intent filters | App architecture
Read Android Developers’ explanation of what PackageManager can discover without starting a component.
In Intent matching, begin at the paragraph starting “Your application can use intent matching in a manner similar to what the Home app does.” Read the query distinction through the explanation that these methods do not activate components. Notice that Activity, service, and broadcast-receiver queries are different APIs; this lesson’s resolution examples concern Activities.
A short demonstration makes the first view concrete. Philipp Lackner’s Intents & Intent Filters - Android Basics 2023 shows package listing on an emulator. Watch only the package-listing segment; the Kotlin and adb work below will go further than the video.
Intents & Intent Filters - Android Basics 2023
Watch the package-listing demonstration in Philipp Lackner’s video to see the difference between installed package identifiers and visible launcher icons.
Watch package listing. Focus on adb shell and pm list packages, and on why an installed package need not appear as an app icon.
Inspect resolution from Kotlin
Start with the same request throughout the lesson. Keeping the action and MIME type fixed makes differences in output easier to interpret:
val request = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
}
queryIntentActivities() asks for matching Activities; resolveActivity() asks for a resolution result. For a comparison with an implicit Activity launch, use MATCH_DEFAULT_ONLY: it limits the query to filters eligible for the CATEGORY_DEFAULT behavior discussed last lesson.
The following diagnostic code uses the Android 13 (API 33) flag overloads. If your app runs on older versions, use the corresponding Int-flags overload in a version-appropriate branch.
fun inspectTextShareHandlers(context: Context) {
val request = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
}
val pm = context.packageManager
val flags = PackageManager.ResolveInfoFlags.of(
PackageManager.MATCH_DEFAULT_ONLY.toLong()
)
val candidates = pm.queryIntentActivities(request, flags)
val resolved = pm.resolveActivity(request, flags)
Log.d("IntentProbe", "Visible matching Activities: ${candidates.size}")
candidates.forEach { result ->
val activity = result.activityInfo
Log.d(
"IntentProbe",
"Candidate: ${activity.packageName}/${activity.name}"
)
}
val selected = resolved?.activityInfo
Log.d(
"IntentProbe",
"Resolution result: " +
if (selected == null) "none visible"
else "${selected.packageName}/${selected.name}"
)
}
Read the two outputs differently. candidates is a list of matches visible to this app. resolved is one resolution result, which can be a system resolver Activity when a choice remains; it is not necessarily one of the share targets the user will ultimately pick. Neither call launches a target, and neither result guarantees that a later launch will succeed.
To inspect declarations rather than matches, use getPackageInfo(). For example, this reports the manifest-declared components in your own installed package:
fun inspectOwnDeclaredComponents(context: Context) {
val pm = context.packageManager
val componentFlags =
PackageManager.GET_ACTIVITIES or
PackageManager.GET_SERVICES or
PackageManager.GET_RECEIVERS or
PackageManager.GET_PROVIDERS
val info = pm.getPackageInfo(
context.packageName,
PackageManager.PackageInfoFlags.of(componentFlags.toLong())
)
info.activities.orEmpty().forEach {
Log.d("ComponentProbe", "Activity: ${it.name}; exported=${it.exported}")
}
info.services.orEmpty().forEach {
Log.d("ComponentProbe", "Service: ${it.name}; exported=${it.exported}")
}
info.receivers.orEmpty().forEach {
Log.d("ComponentProbe", "Receiver: ${it.name}; exported=${it.exported}")
}
info.providers.orEmpty().forEach {
Log.d("ComponentProbe", "Provider: ${it.name}; exported=${it.exported}")
}
}
This inventory does not mean that every listed component accepts the text-share Intent. getPackageInfo() describes installed declarations; the Intent query tests a specific request against eligible filters. It also does not enumerate context-registered receivers, which are not manifest declarations.
You can inspect another package by supplying its package name, but treat a failure or a NameNotFoundException carefully: under package-visibility limits, “my app cannot inspect this package” does not necessarily mean “the package is not installed.” If your feature genuinely needs cross-app discovery, use the narrowly scoped <queries> declaration from the previous lesson. Keep installed-app lists out of production logs unless they are needed for a justified diagnostic purpose.
Inspect the installed device with adb
adb provides a complementary, device-side view. Android Developers’ Android Debug Bridge (adb) reference documents pm package queries and the am commands used to send a test Intent. Read the specified rows and argument list rather than the whole command reference.
Android Debug Bridge (adb) | Android Studio | Android Developers
Read the pm command table to identify what package listing establishes—and what it does not establish about individual Intent handlers.
In Call package manager (pm), find Table 2: Available package manager commands. Read the list packages row, including its -3, -e, and -d options: package-list options. Also locate the path package, enable package_or_component, and disable package_or_component rows. You only need to recognize the last two here; changing component state is not required for this lesson.
Android Debug Bridge (adb) | Android Studio | Android Developers
Read how am start launches an Activity and how command-line arguments specify the same action, category, MIME type, and extras that you set in Kotlin.
In Call activity manager, find Table 1: Available activity manager commands and its start [options] intent row; the launch options cover start and -W. Then, in Specification for intent arguments, read the argument list from its introduction through the string-extra option. Pay particular attention to -a, -d, -t, -c, -n, and --es.
With an emulator or test device connected, use the following as a diagnostic sequence. Replace com.example.intentprobe with the package name of an app you installed for testing.
adb devices
adb shell pm list packages -3
adb shell pm list packages com.example.intentprobe
adb shell pm path com.example.intentprobe
adb shell dumpsys package com.example.intentprobe
The filtered list packages result establishes whether the package is present for the queried device/user. pm path identifies its installed APK path; it says nothing about whether a particular Intent matches. In the potentially long dumpsys package output, search for your package’s Activities and Activity Resolver Table entries. Look for the Activity class, exported state, and the action, category, and MIME type in its filter. Output organization varies by Android version, so search for the class name and action if those section labels differ.
You can also ask the device’s package command to display Activity query and resolution results. First check the commands available on your test device:
adb shell cmd package help
On devices that support query-activities and resolve-activity, use the same Intent specification for both:
adb shell cmd package query-activities --brief \
-a android.intent.action.SEND \
-c android.intent.category.DEFAULT \
-t text/plain
adb shell cmd package resolve-activity --brief \
-a android.intent.action.SEND \
-c android.intent.category.DEFAULT \
-t text/plain
If a device rejects an option, consult its cmd package help output rather than assuming every Android release exposes identical shell syntax. These shell queries are especially useful for spotting a filter you overlooked. Do not expect their output to be identical to your app’s PackageManager output: the shell command and your app are different callers, and your Kotlin query explicitly uses MATCH_DEFAULT_ONLY.
Finally, test launch separately, using harmless sample text:
adb shell am start -W \
-a android.intent.action.SEND \
-c android.intent.category.DEFAULT \
-t text/plain \
--es android.intent.extra.TEXT "Diagnostic sample"
-W waits for the launch attempt to complete. The resulting UI might be a target, a resolver, or an error if nothing can handle the request. A successful am start shows that this device-side launch attempt worked; it does not prove that the user completed a share. Conversely, an unsuccessful shell test deserves investigation of the exact Intent arguments, device state, and component declarations before you conclude that no handler exists.
Reconcile a surprising result
When the Kotlin candidate list, shell output, and observed UI disagree, compare them in this order:
- Confirm identical requests. Check action, category, URI, and MIME type. An extra such as
EXTRA_TEXTdoes not change filter matching. Ensure the Kotlin query usesMATCH_DEFAULT_ONLYwhen comparing it with an Activity launch. - Inspect the installed declaration. Verify that the package and Activity exist, that the component is enabled and accessible to the caller, and that its filter accepts the request. A package name alone is not evidence of a matching Activity.
- Account for the caller. Package-visibility limits apply to discovery from your app.
adbshell observations are diagnostic evidence, not a substitute for observing your app’s own query results. - Observe launch independently. A query is a snapshot, not a reservation. Defaults, resolver UI, or a change in installed or enabled components can affect what happens next.
This is why queryIntentActivities() belongs in features that actually need to display or examine available handlers—not as an obligatory guard before every launch. If the feature only needs to open a handler, retain last lesson’s simpler approach: attempt startActivity() and handle ActivityNotFoundException.
Key takeaways
Use getPackageInfo() to inspect manifest-declared components, queryIntentActivities() to inspect visible matches for a particular Intent, and resolveActivity() to inspect a resolution result. Use adb shell pm and dumpsys package to investigate installation and declarations, cmd package where available to compare device-side resolution, and am start to test an actual launch. Always record the caller and exact Intent when comparing results.
Next, you will move from discovering components to configuring your own Application initialization, with particular attention to Context lifetime and work performed on the main thread.