Lesson 08 / 15
Intents and Intent Filters
The difference between an explicit and an implicit intent, how an intent filter matches on action, category and data, the pending fulfillment when more than one app fulfills the same intent, and the result that changes when the default app is removed.
Published Updated
In this lesson
- Tells the explicit and the implicit intent apart, and the three criteria of an intent filter
- Measures that a filter without the default category leaves the intent unmet
- Shows that an intent matched by more than one app stays pending and carries its condition by name
- Verifies that when the default app is removed the same intent is fulfilled by a substitute or stays pending
Contents
You called a content provider with a URI. The general way of calling another app component is the intent. In the earlier lessons you started the activity, the service and the broadcast receiver in different ways: startActivity, startService, sendBroadcast. The shared input of the three is the Intent object. An intent is a message that says “do this”, and it states its target in two forms.
- Explicit intent: gives the class or the package name of the target app component directly. The platform does no matching; it starts that app component.
- Implicit intent: does not give the target, it states the work to be done: an action, a category and a piece of data. The platform looks at the intent filters of the installed apps and finds the app component that can fulfill the work.
The question of the lesson: when an intent is sent, how does the platform fulfill the request? With an implicit intent this question does not end with a single answer: zero, one or more than one app may match. The lesson measures all three outcomes.
// Fulfillment.kt — classifies how the platform fulfills a request
sealed interface Outcome {
data class Given(val value: String) : Outcome
data class Waiting(val on: String) : Outcome
data class Refused(val reason: String) : Outcome
}
class Ask(val label: String, val wanted: String, val outcome: Outcome) {
val fulfillment: String = when (outcome) {
is Outcome.Waiting -> "pending"
is Outcome.Refused -> "unmet"
is Outcome.Given -> if (outcome.value == wanted) "full" else "substitute"
}
fun report() {
val detail = when (outcome) {
is Outcome.Given -> "given=${outcome.value}"
is Outcome.Waiting -> "waiting-on=${outcome.on}"
is Outcome.Refused -> "reason=${outcome.reason}"
}
println("$label: wanted=$wanted | $detail -> $fulfillment")
}
}
fun tally(asks: List<Ask>) {
for (name in listOf("full", "substitute", "pending", "unmet")) {
println("$name: ${asks.count { it.fulfillment == name }}")
}
}
The Intent Filter
The <intent-filter> element in the manifest file says which intents an app component will receive. A filter carries three criteria.
- Action (
action): the name of the work to be done.VIEWis for showing,SENDis for sending. The action of the intent must be one of the actions declared in the filter. - Category (
category): how the app component can be called. Every category the intent carries must be present in the filter. - Data (
data): the scheme and the type of the content to be processed. If the filter declares data, the data of the intent must agree with it; if the filter declares no data, the intent must carry no data either.
<activity android:name=".ImageViewerActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <data android:scheme="image" /> </intent-filter> </activity>
There is a subtle rule here. When an implicit intent is sent with startActivity, the platform adds the DEFAULT category to the intent by itself. For this reason the filter of an activity that is to be reached with startActivity has to contain the DEFAULT category. Even if the action and the data in the filter are right, without this category there is no match. The rule is often missed: the activity looks as if it works, but it does not reach an implicit intent.
// Platform code: sending an implicit and an explicit intent
import android.content.Intent
import android.net.Uri
val implicit = Intent(Intent.ACTION_VIEW, Uri.parse("geo:41.0,29.0"))
startActivity(implicit)
val explicit = Intent().setClassName("com.example.maps", "com.example.maps.MapActivity")
startActivity(explicit)
An implicit intent shows a special behavior not only when there is no matching app but also when more than one app matches: the platform shows the user a selection screen. If the user makes one app the default, later intents go to that app without asking. This lesson measures these two behaviors.
The Matching Model
The model builds the intent from an action, a data scheme and an optional default choice. The first helper file carries the matching rule and the result rule; the second carries the filters of the sample apps.
// Resolver.kt — builds matching with intent filters, and the result
class Filter(val app: String, val action: String, val categories: Set<String>, val scheme: String?)
fun matches(filter: Filter, action: String, scheme: String?): Boolean =
filter.action == action && "DEFAULT" in filter.categories && filter.scheme == scheme
fun resolve(action: String, scheme: String?, filters: List<Filter>, chosen: String?): Outcome {
val hits = filters.filter { matches(it, action, scheme) }.map { it.app }.sorted()
return when {
hits.isEmpty() -> Outcome.Refused("no-match")
hits.size == 1 -> Outcome.Given(hits[0])
chosen != null && chosen in hits -> Outcome.Given(chosen)
else -> Outcome.Waiting("user-choice")
}
}
fun resolveExplicit(app: String, installed: Set<String>): Outcome =
if (app in installed) Outcome.Given(app) else Outcome.Refused("app-missing")
// Installed.kt — filters of the sample apps
val gallery = Filter("gallery", "VIEW", setOf("DEFAULT"), "image")
val editor = Filter("editor", "VIEW", setOf("DEFAULT"), "image")
val camera = Filter("camera", "VIEW", setOf("DEFAULT"), "image")
val maps = Filter("maps", "VIEW", setOf("DEFAULT"), "geo")
val browser = Filter("browser", "VIEW", setOf("DEFAULT", "BROWSABLE"), "https")
val browseOnly = Filter("browse-only", "VIEW", setOf("BROWSABLE"), "image")
resolve does not model the whole detail of the platform; it carries the three criteria (action, the DEFAULT category, scheme) and the result rule. The result rule is this: zero matches is unmet, a single match is that app, and with more than one match, if the default is chosen and is among the matches then that app, otherwise a pending fulfillment waiting for the user’s choice.
AC1. The model builds the data criterion with the scheme only. The data type (mimeType), path patterns and the combination of more than one <data> element were not measured. Of the categories only DEFAULT is checked.
Explicit and Implicit Intents
The first measurement makes four requests. The explicit intent calls the installed app directly by name. The implicit intent looks for a match with a scheme and an action. The last two requests stay unmet: one explicitly asks for an app that is not installed, the other sends a scheme that is in no app’s filter.
// Main.kt — explicit and implicit intents
fun main() {
val installed = setOf("gallery", "maps", "browser")
val filters = listOf(gallery, maps, browser)
val asks = listOf(
Ask("explicit", "maps", resolveExplicit("maps", installed)),
Ask("implicit-geo", "maps", resolve("VIEW", "geo", filters, null)),
Ask("implicit-ftp", "browser", resolve("VIEW", "ftp", filters, null)),
Ask("explicit-missing", "music", resolveExplicit("music", installed)),
)
for (ask in asks) {
ask.report()
}
tally(asks)
}
explicit: wanted=maps | given=maps -> full implicit-geo: wanted=maps | given=maps -> full implicit-ftp: wanted=browser | reason=no-match -> unmet explicit-missing: wanted=music | reason=app-missing -> unmet full: 2 substitute: 0 pending: 0 unmet: 2
The explicit intent and the matching implicit intent were fulfilled in full: both reached the same app. The implicit intent stayed unmet because the ftp scheme is in no filter; so did the explicit intent that calls an app that is not installed by name. The difference between the two unmet results shows in the reason: no-match is the absence of the match the platform looked for, while app-missing is the absence of the target itself.
The practical result: the code that sends an implicit intent must take into account that there may be no match. In this case the platform throws ActivityNotFoundException. The sender has to check beforehand with resolveActivity or with a package visibility declaration.
AC2. The model has no package visibility. In reality an app has to declare a <queries> element in the manifest file to see whether other apps are installed; this was not measured.
A Missing Category in the Filter
A filter can carry the right action and the right scheme and still not reach the intent. A missing DEFAULT category is the most common reason for this. The browse-only app has declared the VIEW action and the image scheme, but its filter has only the BROWSABLE category.
// Main.kt — the default category
fun main() {
val asks = listOf(
Ask("category-present", "gallery", resolve("VIEW", "image", listOf(gallery), null)),
Ask("category-missing", "browse-only", resolve("VIEW", "image", listOf(browseOnly), null)),
Ask("action-missing", "gallery", resolve("SEND", "image", listOf(gallery), null)),
)
for (ask in asks) {
ask.report()
}
tally(asks)
}
category-present: wanted=gallery | given=gallery -> full category-missing: wanted=browse-only | reason=no-match -> unmet action-missing: wanted=gallery | reason=no-match -> unmet full: 1 substitute: 0 pending: 0 unmet: 2
The filter that carries the DEFAULT category was fulfilled in full. The filter that does not carry the category stayed unmet although its action and scheme were right. The intent whose action did not hold stayed unmet too. The cause of both shows under the same label (no-match); to find the cause, the three criteria of the filter are looked at one by one.
AC3. The BROWSABLE category is required only for links that come from the browser, and it is not added by itself to an implicit intent that comes with startActivity. The model builds this distinction with a single rule, the DEFAULT check.
More Than One Match
If two apps can fulfill the same intent, the platform leaves the choice to the user. The selection screen has been shown but the user has not chosen yet: the request has been neither fulfilled nor refused. This is a pending fulfillment, and the condition it waits on is user-choice.
// Main.kt — a single match, many matches, a default
fun main() {
val filters = listOf(gallery, editor, maps)
val asks = listOf(
Ask("single-match", "maps", resolve("VIEW", "geo", filters, null)),
Ask("many-matches", "gallery", resolve("VIEW", "image", filters, null)),
Ask("default-set", "gallery", resolve("VIEW", "image", filters, "gallery")),
)
for (ask in asks) {
ask.report()
}
tally(asks)
}
single-match: wanted=maps | given=maps -> full many-matches: wanted=gallery | waiting-on=user-choice -> pending default-set: wanted=gallery | given=gallery -> full full: 2 substitute: 0 pending: 1 unmet: 0
The single match is full. Because two apps fit the image scheme, the second request stayed pending; the code on the caller’s side cannot see the selection screen, it sees only that startActivity returned. In the third request the user has made gallery the default, and the platform started that app without asking for a choice: full.
Earlier lessons kept “deferred” and “pending” apart. The same distinction holds here: the selection screen is not a deferral but a fulfillment waiting on a condition with a known name. The condition is carried as user-choice, and when the condition is met the result becomes full or a substitute.
AC4. The model carries the default choice as a single name. In reality the default is recorded separately by intent type and by data scheme; this was not measured.
When the Default App Is Removed
The app chosen as the default can be removed. In this case the same intent now gives another result. The user has made gallery the default; later gallery is deleted. If editor remains, there is a single match and the intent goes to it: the user wanted the gallery, the editor was given. If two apps remain, the choice is asked again.
// Main.kt — when the default app is removed
fun main() {
val asks = listOf(
Ask("still-installed", "gallery", resolve("VIEW", "image", listOf(gallery, editor), "gallery")),
Ask("one-left", "gallery", resolve("VIEW", "image", listOf(editor), "gallery")),
Ask("two-left", "gallery", resolve("VIEW", "image", listOf(editor, camera), "gallery")),
)
for (ask in asks) {
ask.report()
}
tally(asks)
}
still-installed: wanted=gallery | given=gallery -> full one-left: wanted=gallery | given=editor -> substitute two-left: wanted=gallery | waiting-on=user-choice -> pending full: 1 substitute: 1 pending: 1 unmet: 0
If the default stays, the result is full. When the default is gone and a single app remains, the request was fulfilled by a substitute: the intent did not fail, but it did not go to the app that was wanted. When two apps remain, the platform goes back to asking the user again: pending. The same intent, the same code, got three different fulfillments as the set of installed apps changed.
The Bounding Measurement
The same intent, the same filters: only the number of installed apps changes. Zero apps is unmet, one app is full, two apps is pending.
// Main.kt — the same intent, three sets of installed apps
fun main() {
val asks = listOf(
Ask("none-installed", "gallery", resolve("VIEW", "image", emptyList(), null)),
Ask("one-installed", "gallery", resolve("VIEW", "image", listOf(gallery), null)),
Ask("two-installed", "gallery", resolve("VIEW", "image", listOf(gallery, editor), null)),
)
for (ask in asks) {
ask.report()
}
tally(asks)
println(asks.map { it.fulfillment })
}
none-installed: wanted=gallery | reason=no-match -> unmet one-installed: wanted=gallery | given=gallery -> full two-installed: wanted=gallery | waiting-on=user-choice -> pending full: 1 substitute: 0 pending: 1 unmet: 1 [unmet, full, pending]
Three requests, three different fulfillments. The fulfillment of this lesson is pending, because the result of an implicit intent comes not from the intent but from the apps installed on the device. The sending code cannot know in advance which result will come; the only reliable way is to look with resolveActivity and to be ready for all three results.
AC5. The model takes the apps to be a set installed on the device. Multi-user profiles, the work profile and whether an app is enabled or disabled were not measured.
Summary
- This lesson’s criterion: fulfillment. An explicit intent gives its target by name; an implicit intent looks for a match by action, category and data. This lesson’s fulfillment: pending; the result of an implicit intent depends on the installed apps.
startActivityadds theDEFAULTcategory to an implicit intent; if the filter lacks this category there is no match: unmet.- If more than one app matches, the fulfillment stays pending and its condition is
user-choice. If a default is chosen, full. - When the default app is removed, a single remaining app gives a substitute and two remaining apps give pending.
- The same intent gets three different fulfillments by the set of installed apps: unmet, full, pending.
Next Step
When an activity is started with an intent, the platform places it in a task. The next lesson, Tasks and the Back Stack, measures whether the launch modes create a new instance or use the existing one, and how the back stack looks at every step.
To keep your progress and take notes, Log in
My notes
Log in to take notes.