In the Linkify! article, we talked about
using Linkify to turn wiki words (those that match a regular expression that we
have defined) into a content:
URI and defining a path to data that
matched a note belonging to that wiki word. As an example, a matching word like
ToDoList
would be turned into a URI such as
content://com.google.android.wikinotes.db.wikinotes/wikinotes/ToDoList
and then acted upon using the VIEW action from the Linkify class.
This article examines how the Android system takes this combination of
VIEW
action and content:
URI and finds the correct
activity to fire in order to do something with the data. It will also explain
how the other default links created by Linkify, such as web URLs and telephone
numbers, also result in the correct activity to handle that data type being
fired. Finally, this article will start to examine the custom
ContentProvider
that has been created to handle WikiNotes data. The
full description of the ContentProvider and what it does will span a couple more
articles as well, because there is a lot to cover.
At a high level, the steps for Linkify to invoke an intent, and for the resulting activity (if any) to handle it, look like this:
This is actually a simpler process than it sounds, and it is quite lightweight as well. Perhaps a more understandable statement about how it works might be:
Linkify is used to turn matching text into hot-links. When the user selects a hot-link, Android takes the data locator represented by the hot-link and looks for a data handler for that data locator. If it finds one, it asks for what type of data is returned for that locator. It then looks for something registered with the system that handles that type of data for the VIEW action, and starts it, including the data locator in the request.
The real key here is the MIME type. MIME stands for Multipurpose Internet Mail Extensions — a standard for sending attachments over email. The MIME type (which is the part Android uses) is a way of describing certain kinds of data. That type is then used to look for an Activity that can do something with that data type. In this way, ContentProviders and Activities (or other IntentReceivers) are decoupled, meaning that a given Content URI might have a different ContentProvider to handle it, but could still use the same MIME type meaning that the same activity could be called upon to handle the resulting data.
Using the above workflow, let's take a look at exactly how the process works in WikiNotes for Android:
First, Linkify is used to turn text matching the wiki word regular expression
into a link that provides a Content URI for that wiki word, for example
content://com.google.android.wikinotes.db.wikinotes/wikinotes/ToDoList
.
When the user clicks on the wiki word link, Linkify invokes the VIEW action on the Content URI. At this point, the Android system takes over getting the Intent request to the correct activity.
Next, Android looks for a ContentProvider that has been registered with the system to handle URIs matching our Content URI format.
In our case, we have a definition inside our application's AndroidManifest.xml file that reads:
<provider name="com.google.android.wikinotes.db.WikiNotesProvider" android:authorities="com.google.android.wikinotes.db.wikinotes" />
This establishes that we have a ContentProvider defined in our application
that provides the "root authority":
com.google.android.wikinotes.db.wikinotes
. This is the first part
of the Content URI that we create for a wiki word link. Root Authority is just
another way of thinking about a descriptor that is registered with Android to
allow requests for certain URLs to be routed to the correct class.
So, the whole definition is that a class called
com.google.android.wikinotes.db.WikiNotesProvider
is registered
with the system as able to handle the
com.google.android.wikinotes.db.wikinotes
root authority (i.e. URIs
starting with that identifier).
From here, Android takes the rest of the URI and presents it to that ContentProvider. If you look at the WikiNotesProvider class and scroll to the very bottom, in the static block there, you can see the pattern definitions to match the rest of the URL.
In particular, take a look at the two lines:
URI_MATCHER.addURI(WikiNote.WIKINOTES_AUTHORITY, "wikinotes", NOTES); URI_MATCHER.addURI(WikiNote.WIKINOTES_AUTHORITY, "wikinotes/*", NOTE_NAME);
These are the definitions of URIs that our ContentProvider recognizes and can
handle. The first recognizes a full URI of
content://com.google.android.wikinotes.db.wikinotes/wikinotes
and
associates that with a constant called NOTES. This is used elsewhere in the
ContentProvider to provide a list of all of the wiki notes in the database when
the URI is requested.
The second line uses a wildcard — '*' — to match a request of the
form that Linkify will create, e.g.
content://com.google.android.wikinotes.db.wikinotes/wikinotes/ToDoList
. In this example, the * matches the ToDoList part of the URI and is
available to the handler of the request, so that it can fish out the matching
note for ToDoList and return it as the data. This also associates that match
with a constant called NOTE_NAME, which again is used as an identifier elsewhere
in the ContentProvider.
The other matches in this static block are related to forms of searching that have been implemented in the WikiNotes for Android application, and will be covered in later articles. Likewise, how the data is obtained from this matching pattern will be the subject of the next article.
For right now we are concerned with the MIME type for the URI. This is
defined in the getType()
method also in the
WikiNotesProvider
class (about halfway through the file). Take a quick look at this. The key
parts for now are:
case NOTES: return "vnd.android.cursor.dir/vnd.google.wikinote";
and
case NOTE_NAME: return "vnd.android.cursor.item/vnd.google.wikinote";
These are the same constant names we defined in our pattern
matchers. In the first case, that of the all notes URI, the MIME type
returned is vnd.android.cursor.dir/vnd.google.wikinote
which is like saying an Android list (dir) of Google wiki notes (the
vnd bit is MIME-speak for "vendor specific definition"). Likewise, in
the case of a NOTE_NAME match, the MIME type returned is
vnd.android.cursor.item/vnd.google.wikinote
which is
like saying an Android item of Google wiki notes.
Note that if you define your own MIME data types like this, the
vnd.android.cursor.dir
and vnd.android.cursor.item
categories should be retained, since they have meaning to the Android
system, but the actual item types should be changed to reflect your
particular data type.
So far Android has been able to find a ContentProvider that handles the Content URI supplied by the Linkify Intent call, and has queried the ContentProvider to find out the MIME types for that URI. The final step is to find an activity that can handle the VIEW action for that MIME type. Take a look in the the AndroidManifest.xml file again. Inside the WikiNotes activity definition, you will see:
<intent-filter> <action name="android.intent.action.VIEW"/> <category name="android.intent.category.DEFAULT"/> <category name="android.intent.category.BROWSABLE"/> <data mimetype="vnd.android.cursor.item/vnd.google.wikinote"/> </intent-filter>
This is the correct combination of matches for the VIEW action on a WikiNote type that is requested from the LINKIFY class. The DEFAULT category indicates that the WikiNotes activity should be treated as a default handler (a primary choice) for this kind of data, and the BROWSABLE category means it can be invoked from a "browser", in this case the marked-up Linkified text.
Using this information, Android can match up the VIEW action request for the WikiNotes data type with the WikiNotes activity, and can then use the WikiNotes activity to handle the request.
It's quite a trip through the system, and there is a lot to absorb here, but this is one of the main reasons I wanted to write WikiNotes in the first place. If you follow and understand the steps here, you'll have a good grasp of the whole Intents mechanism in Android, and how it helps loosely coupled activities cooperate to get things done.
In this case, we could have found another way to detect wiki words based on a regular expression, and maybe written our own handler to intercept clicks within the TextView and dig out the right data and display it. This would seem to accomplish the same functionality just as easily as using intents, so what is the advantage to using the full Intents mechanism?
In fact there are several advantages:
The most obvious is that because we are using the standard Intent based approach, we are not limited to just linking and navigating to other wiki notes. We get similar behavior to a number of other data types as well. For example, a telephone number or web URL in a wiki note will be marked up by Linkify, and using this same mechanism (VIEW action on the linked data type) the browser or dialer activities will be automatically fired.
It also means that each operation on a wiki note can be treated as a separate life cycle by our activity. We are not dealing with swapping data in and out of an existing activity - each activity works on a particular wiki note and that's all you have to worry about.
Another advantage is that we now have a public activity to handle VIEW actions in WikiNotes no matter where the request comes from. Another application could request to view a wiki note (perhaps without even knowing what kind of data it is) and our activity could start up and handle it.
The backstack is automatically maintained for you too. As you forward navigate through WikiNotes, Android maintains the history of notes visited, and so when you hit the back button you go back to the last note you were on. All this is free because we rely on the Android intents mechanism.
Finally, if you run WikiNotes for Android and then start DDMS to take a look at the Activity threads in the WikiNotes application while it is running, you can see that despite what you might think, letting Android manage the navigation is very efficient. Create a few linked notes, as many links deep as you like, and then follow them. If you follow links hundreds of notes deep, you will still only see a handful of WikiNotes activities. Android is managing the activities, closing the older ones as necessary and using the life cycle to swap data in and out.
This was a long article, but necessarily so. It demonstrates the importance of the Intents mechanism and to reinforce the notion that it should be used whenever possible for forward navigation, even within a single application. Illustrating this is one of the primary reasons I wrote WikiNotes for Android in the first place.
In the next article we will look deeper into the ContentProvider and examine how it turns a Content URI into a row (or several rows) of data that can be used by an activity.