1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
|
page.title=WikiNotes: Routing Intents
@jd:body
<p>In <a href="wikinotes-linkify.html">the Linkify! article</a>, we talked about
using Linkify to turn wiki words (those that match a regular expression that we
have defined) into a <code>content:</code> URI and defining a path to data that
matched a note belonging to that wiki word. As an example, a matching word like
<code>ToDoList</code> would be turned into a URI such as
<code>content://com.google.android.wikinotes.db.wikinotes/wikinotes/ToDoList
</code> and then acted upon using the VIEW action from the Linkify class.</p>
<p>This article examines how the Android system takes this combination of
<code>VIEW</code> action and <code>content:</code> 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
<code>ContentProvider</code> 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.</p>
<h3>The Linkify-calls-intent Workflow</h3>
<p>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:</p>
<ol>
<li>Linkify is invoked on a TextView to turn matching text patterns into Intent links.</li>
<li>Linkify takes over monitoring for those Intent links being selected by the user.</li>
<li>When the user selects a link, Linkify calls the VIEW action using the content: URI associated with the link.</li>
<li>Android takes the content: URI that represents the data, and looks for a
ContentProvider registered in the system that matches the URI.</li>
<li>If a match is found, Android queries the ContentProvider using the URI,
and asks what MIME type the data that will be returned from the URI is.</li>
<li>Android then looks for an activity registered in the system with an
intent-filter that matches both the VIEW action, and the MIME type for
the data represented by the content: URI.</li>
<li>Assuming a match is found, Linkify then invokes the intent for
the URI, at which point the activity takes over, and is handed
the content: URI.</li>
<li>The activity can then use the URI to retrieve the data and act on
it.</li>
</ol>
<p>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:</p>
<p>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.</p>
<p>The real key here is the MIME type. MIME stands for <a
href="http://en.wikipedia.org/wiki/MIME">Multipurpose Internet Mail
Extensions</a> — 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.</p>
<h3>Linkify on a wiki word</h3>
<p>Using the above workflow, let's take a look at exactly how the process
works in WikiNotes for Android:</p>
<p>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
<code>content://com.google.android.wikinotes.db.wikinotes/wikinotes/ToDoList</code>.</p>
<p>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.</p>
<p>Next, Android looks for a ContentProvider that has been registered
with the system to handle URIs matching our Content URI format.</p>
<p>In our case, we have a definition inside
<a href="http://code.google.com/p/apps-for-android/source/browse/trunk/WikiNotes/AndroidManifest.xml">our application's AndroidManifest.xml</a>
file that reads:</p>
<pre><provider name="com.google.android.wikinotes.db.WikiNotesProvider"
android:authorities="com.google.android.wikinotes.db.wikinotes" /></pre>
<p>This establishes that we have a ContentProvider defined in our application
that provides the "root authority":
<code>com.google.android.wikinotes.db.wikinotes</code>. 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.</p>
<p>So, the whole definition is that a class called
<code>com.google.android.wikinotes.db.WikiNotesProvider</code> is registered
with the system as able to handle the
<code>com.google.android.wikinotes.db.wikinotes</code> root authority (i.e. URIs
starting with that identifier).</p>
<p>From here, Android takes the rest of the URI and presents it to that
ContentProvider. If you look at the
<a href="http://code.google.com/p/apps-for-android/source/browse/trunk/WikiNotes/src/com/google/android/wikinotes/db/WikiNotesProvider.java">WikiNotesProvider
class</a> and scroll to the very bottom, in the static block there, you can see
the pattern definitions to match the rest of the URL.</p>
<p>In particular, take a look at the two lines:</p>
<pre>URI_MATCHER.addURI(WikiNote.WIKINOTES_AUTHORITY, "wikinotes", NOTES);
URI_MATCHER.addURI(WikiNote.WIKINOTES_AUTHORITY, "wikinotes/*", NOTE_NAME);</pre>
<p>These are the definitions of URIs that our ContentProvider recognizes and can
handle. The first recognizes a full URI of
<code>content://com.google.android.wikinotes.db.wikinotes/wikinotes</code> 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.</p>
<p>The second line uses a wildcard — '*' — to match a request of the
form that Linkify will create, e.g.
<code>content://com.google.android.wikinotes.db.wikinotes/wikinotes/ToDoList
</code>. 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.</p>
<p>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.</p>
<p>For right now we are concerned with the MIME type for the URI. This is
defined in the <code>getType()</code> method also in the
<a href="http://code.google.com/p/apps-for-android/source/browse/trunk/WikiNotes/src/com/google/android/wikinotes/db/WikiNotesProvider.java">WikiNotesProvider
class</a> (about halfway through the file). Take a quick look at this. The key
parts for now are:</p>
<pre>case NOTES:
return "vnd.android.cursor.<b>dir</b>/vnd.google.wikinote";</pre>
<p>and</p>
<pre>case NOTE_NAME:
return "vnd.android.cursor.<b>item</b>/vnd.google.wikinote";</pre>
<p>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 <code>vnd.android.cursor.dir/vnd.google.wikinote</code>
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
<code>vnd.android.cursor.item/vnd.google.wikinote</code> which is
like saying an Android item of Google wiki notes.</p>
<p>Note that if you define your own MIME data types like this, the
<code>vnd.android.cursor.dir</code> and <code>vnd.android.cursor.item</code>
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.</p>
<p>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
<a href="http://code.google.com/p/apps-for-android/source/browse/trunk/WikiNotes/AndroidManifest.xml">AndroidManifest.xml file</a>
again. Inside the WikiNotes activity definition, you will see:</p>
<pre><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></pre>
<p>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.</p>
<p>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.</p>
<h3>Why do it like this?</h3>
<p>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.</p>
<p>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?</p>
<p>In fact there are several advantages:</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<h3>Next Time</h3>
<p>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.</p>
<p>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.</p>
|