Dirk Dougherty | 22558d0 | 2009-12-10 16:25:06 -0800 | [diff] [blame] | 1 | page.title=WikiNotes: Routing Intents |
Scott Main | 796ce77 | 2011-02-16 10:04:45 -0800 | [diff] [blame] | 2 | parent.title=Articles |
| 3 | parent.link=../browser.html?tag=article |
Dirk Dougherty | 22558d0 | 2009-12-10 16:25:06 -0800 | [diff] [blame] | 4 | @jd:body |
| 5 | |
| 6 | |
| 7 | <p>In <a href="wikinotes-linkify.html">the Linkify! article</a>, we talked about |
| 8 | using Linkify to turn wiki words (those that match a regular expression that we |
| 9 | have defined) into a <code>content:</code> URI and defining a path to data that |
| 10 | matched a note belonging to that wiki word. As an example, a matching word like |
| 11 | <code>ToDoList</code> would be turned into a URI such as |
| 12 | <code>content://com.google.android.wikinotes.db.wikinotes/wikinotes/ToDoList |
| 13 | </code> and then acted upon using the VIEW action from the Linkify class.</p> |
| 14 | |
| 15 | <p>This article examines how the Android system takes this combination of |
| 16 | <code>VIEW</code> action and <code>content:</code> URI and finds the correct |
| 17 | activity to fire in order to do something with the data. It will also explain |
| 18 | how the other default links created by Linkify, such as web URLs and telephone |
| 19 | numbers, also result in the correct activity to handle that data type being |
| 20 | fired. Finally, this article will start to examine the custom |
| 21 | <code>ContentProvider</code> that has been created to handle WikiNotes data. The |
| 22 | full description of the ContentProvider and what it does will span a couple more |
| 23 | articles as well, because there is a lot to cover.</p> |
| 24 | |
| 25 | <h3>The Linkify-calls-intent Workflow</h3> |
| 26 | |
| 27 | <p>At a high level, the steps for Linkify to invoke an intent, and for the |
| 28 | resulting activity (if any) to handle it, look like this:</p> |
| 29 | |
| 30 | <ol> |
| 31 | <li>Linkify is invoked on a TextView to turn matching text patterns into Intent links.</li> |
| 32 | <li>Linkify takes over monitoring for those Intent links being selected by the user.</li> |
| 33 | <li>When the user selects a link, Linkify calls the VIEW action using the content: URI associated with the link.</li> |
| 34 | <li>Android takes the content: URI that represents the data, and looks for a |
| 35 | ContentProvider registered in the system that matches the URI.</li> |
| 36 | <li>If a match is found, Android queries the ContentProvider using the URI, |
| 37 | and asks what MIME type the data that will be returned from the URI is.</li> |
| 38 | <li>Android then looks for an activity registered in the system with an |
| 39 | intent-filter that matches both the VIEW action, and the MIME type for |
| 40 | the data represented by the content: URI.</li> |
| 41 | <li>Assuming a match is found, Linkify then invokes the intent for |
| 42 | the URI, at which point the activity takes over, and is handed |
| 43 | the content: URI.</li> |
| 44 | <li>The activity can then use the URI to retrieve the data and act on |
| 45 | it.</li> |
| 46 | </ol> |
| 47 | |
| 48 | <p>This is actually a simpler process than it |
| 49 | sounds, and it is quite lightweight as well. Perhaps a more |
| 50 | understandable statement about how it works might be:</p> |
| 51 | |
| 52 | <p>Linkify is used to turn matching text into hot-links. When the user |
| 53 | selects a hot-link, Android takes the data locator represented by the |
| 54 | hot-link and looks for a data handler for that data locator. If it |
| 55 | finds one, it asks for what type of data is returned for that locator. |
| 56 | It then looks for something registered with the system that handles |
| 57 | that type of data for the VIEW action, and starts it, including the |
| 58 | data locator in the request.</p> |
| 59 | |
| 60 | <p>The real key here is the MIME type. MIME stands for <a |
| 61 | href="http://en.wikipedia.org/wiki/MIME">Multipurpose Internet Mail |
| 62 | Extensions</a> — a standard for sending attachments over email. The MIME |
| 63 | type (which is the part Android uses) is a way of describing certain kinds of |
| 64 | data. That type is then used to look for an Activity that can do something with |
| 65 | that data type. In this way, ContentProviders and Activities (or other |
| 66 | IntentReceivers) are decoupled, meaning that a given Content URI might have a |
| 67 | different ContentProvider to handle it, but could still use the same MIME type |
| 68 | meaning that the same activity could be called upon to handle the resulting |
| 69 | data.</p> |
| 70 | |
| 71 | <h3>Linkify on a wiki word</h3> |
| 72 | |
| 73 | <p>Using the above workflow, let's take a look at exactly how the process |
| 74 | works in WikiNotes for Android:</p> |
| 75 | |
| 76 | <p>First, Linkify is used to turn text matching the wiki word regular expression |
| 77 | into a link that provides a Content URI for that wiki word, for example |
| 78 | <code>content://com.google.android.wikinotes.db.wikinotes/wikinotes/ToDoList</code>.</p> |
| 79 | |
| 80 | <p>When the user clicks on the wiki word link, Linkify invokes the VIEW |
| 81 | action on the Content URI. At this point, the Android system takes over |
| 82 | getting the Intent request to the correct activity.</p> |
| 83 | |
| 84 | <p>Next, Android looks for a ContentProvider that has been registered |
| 85 | with the system to handle URIs matching our Content URI format.</p> |
| 86 | |
| 87 | <p>In our case, we have a definition inside |
| 88 | <a href="http://code.google.com/p/apps-for-android/source/browse/trunk/WikiNotes/AndroidManifest.xml">our application's AndroidManifest.xml</a> |
| 89 | file that reads:</p> |
| 90 | |
| 91 | <pre><provider name="com.google.android.wikinotes.db.WikiNotesProvider" |
| 92 | android:authorities="com.google.android.wikinotes.db.wikinotes" /></pre> |
| 93 | |
| 94 | <p>This establishes that we have a ContentProvider defined in our application |
| 95 | that provides the "root authority": |
| 96 | <code>com.google.android.wikinotes.db.wikinotes</code>. This is the first part |
| 97 | of the Content URI that we create for a wiki word link. Root Authority is just |
| 98 | another way of thinking about a descriptor that is registered with Android to |
| 99 | allow requests for certain URLs to be routed to the correct class.</p> |
| 100 | |
| 101 | <p>So, the whole definition is that a class called |
| 102 | <code>com.google.android.wikinotes.db.WikiNotesProvider</code> is registered |
| 103 | with the system as able to handle the |
| 104 | <code>com.google.android.wikinotes.db.wikinotes</code> root authority (i.e. URIs |
| 105 | starting with that identifier).</p> |
| 106 | |
| 107 | <p>From here, Android takes the rest of the URI and presents it to that |
| 108 | ContentProvider. If you look at the |
| 109 | <a href="http://code.google.com/p/apps-for-android/source/browse/trunk/WikiNotes/src/com/google/android/wikinotes/db/WikiNotesProvider.java">WikiNotesProvider |
| 110 | class</a> and scroll to the very bottom, in the static block there, you can see |
| 111 | the pattern definitions to match the rest of the URL.</p> |
| 112 | |
| 113 | <p>In particular, take a look at the two lines:</p> |
| 114 | |
| 115 | <pre>URI_MATCHER.addURI(WikiNote.WIKINOTES_AUTHORITY, "wikinotes", NOTES); |
| 116 | URI_MATCHER.addURI(WikiNote.WIKINOTES_AUTHORITY, "wikinotes/*", NOTE_NAME);</pre> |
| 117 | |
| 118 | <p>These are the definitions of URIs that our ContentProvider recognizes and can |
| 119 | handle. The first recognizes a full URI of |
| 120 | <code>content://com.google.android.wikinotes.db.wikinotes/wikinotes</code> and |
| 121 | associates that with a constant called NOTES. This is used elsewhere in the |
| 122 | ContentProvider to provide a list of all of the wiki notes in the database when |
| 123 | the URI is requested.</p> |
| 124 | |
| 125 | <p>The second line uses a wildcard — '*' — to match a request of the |
| 126 | form that Linkify will create, e.g. |
| 127 | <code>content://com.google.android.wikinotes.db.wikinotes/wikinotes/ToDoList |
| 128 | </code>. In this example, the * matches the ToDoList part of the URI and is |
| 129 | available to the handler of the request, so that it can fish out the matching |
| 130 | note for ToDoList and return it as the data. This also associates that match |
| 131 | with a constant called NOTE_NAME, which again is used as an identifier elsewhere |
| 132 | in the ContentProvider.</p> |
| 133 | |
| 134 | <p>The other matches in this static block are related to forms of |
| 135 | searching that have been implemented in the WikiNotes for Android |
| 136 | application, and will be covered in later articles. Likewise, how the |
| 137 | data is obtained from this matching pattern will be the subject of the |
| 138 | next article.</p> |
| 139 | |
| 140 | <p>For right now we are concerned with the MIME type for the URI. This is |
| 141 | defined in the <code>getType()</code> method also in the |
| 142 | <a href="http://code.google.com/p/apps-for-android/source/browse/trunk/WikiNotes/src/com/google/android/wikinotes/db/WikiNotesProvider.java">WikiNotesProvider |
| 143 | class</a> (about halfway through the file). Take a quick look at this. The key |
| 144 | parts for now are:</p> |
| 145 | |
| 146 | <pre>case NOTES: |
| 147 | return "vnd.android.cursor.<b>dir</b>/vnd.google.wikinote";</pre> |
| 148 | |
| 149 | <p>and</p> |
| 150 | |
| 151 | <pre>case NOTE_NAME: |
| 152 | return "vnd.android.cursor.<b>item</b>/vnd.google.wikinote";</pre> |
| 153 | |
| 154 | <p>These are the same constant names we defined in our pattern |
| 155 | matchers. In the first case, that of the all notes URI, the MIME type |
| 156 | returned is <code>vnd.android.cursor.dir/vnd.google.wikinote</code> |
| 157 | which is like saying an Android list (dir) of Google wiki notes (the |
| 158 | vnd bit is MIME-speak for "vendor specific definition"). Likewise, in |
| 159 | the case of a NOTE_NAME match, the MIME type returned is |
| 160 | <code>vnd.android.cursor.item/vnd.google.wikinote</code> which is |
| 161 | like saying an Android item of Google wiki notes.</p> |
| 162 | |
| 163 | <p>Note that if you define your own MIME data types like this, the |
| 164 | <code>vnd.android.cursor.dir</code> and <code>vnd.android.cursor.item</code> |
| 165 | categories should be retained, since they have meaning to the Android |
| 166 | system, but the actual item types should be changed to reflect your |
| 167 | particular data type.</p> |
| 168 | |
| 169 | <p>So far Android has been able to find a ContentProvider that handles |
| 170 | the Content URI supplied by the Linkify Intent call, and has queried |
| 171 | the ContentProvider to find out the MIME types for that URI. The final |
| 172 | step is to find an activity that can handle the VIEW action for that |
| 173 | MIME type. Take a look in the the |
| 174 | <a href="http://code.google.com/p/apps-for-android/source/browse/trunk/WikiNotes/AndroidManifest.xml">AndroidManifest.xml file</a> |
| 175 | again. Inside the WikiNotes activity definition, you will see:</p> |
| 176 | |
| 177 | <pre><intent-filter> |
| 178 | <action name="android.intent.action.VIEW"/> |
| 179 | <category name="android.intent.category.DEFAULT"/> |
| 180 | <category name="android.intent.category.BROWSABLE"/> |
| 181 | <data mimetype="vnd.android.cursor.item/vnd.google.wikinote"/> |
| 182 | </intent-filter></pre> |
| 183 | |
| 184 | <p>This is the correct combination of matches for the VIEW action on a |
| 185 | WikiNote type that is requested from the LINKIFY class. The DEFAULT |
| 186 | category indicates that the WikiNotes activity should be treated as a |
| 187 | default handler (a primary choice) for this kind of data, and the |
| 188 | BROWSABLE category means it can be invoked from a "browser", in this |
| 189 | case the marked-up Linkified text.</p> |
| 190 | |
| 191 | <p>Using this information, Android can match up the VIEW action request |
| 192 | for the WikiNotes data type with the WikiNotes activity, and can then |
| 193 | use the WikiNotes activity to handle the request.</p> |
| 194 | |
| 195 | <h3>Why do it like this?</h3> |
| 196 | |
| 197 | <p>It's quite a trip through the system, and there is a lot to absorb |
| 198 | here, but this is one of the main reasons I wanted to write WikiNotes |
| 199 | in the first place. If you follow and understand the steps here, you'll |
| 200 | have a good grasp of the whole Intents mechanism in Android, and how it |
| 201 | helps loosely coupled activities cooperate to get things done.</p> |
| 202 | |
| 203 | <p>In this case, we could have found another way to detect wiki words |
| 204 | based on a regular expression, and maybe written our own handler to |
| 205 | intercept clicks within the TextView and dig out the right data and |
| 206 | display it. This would seem to accomplish the same functionality just |
| 207 | as easily as using intents, so what is the advantage to using the full |
| 208 | Intents mechanism?</p> |
| 209 | |
| 210 | <p>In fact there are several advantages:</p> |
| 211 | |
| 212 | <p>The most obvious is that because we are using the standard Intent |
| 213 | based approach, we are not limited to just linking and navigating to |
| 214 | other wiki notes. We get similar behavior to a number of other data |
| 215 | types as well. For example, a telephone number or web URL in a wiki |
| 216 | note will be marked up by Linkify, and using this same mechanism (VIEW |
| 217 | action on the linked data type) the browser or dialer activities will |
| 218 | be automatically fired.</p> |
| 219 | |
| 220 | <p>It also means that each operation on a wiki note can be treated as a |
| 221 | separate life cycle by our activity. We are not dealing with swapping |
| 222 | data in and out of an existing activity - each activity works on a |
| 223 | particular wiki note and that's all you have to worry about.</p> |
| 224 | |
| 225 | <p>Another advantage is that we now have a public activity to handle |
| 226 | VIEW actions in WikiNotes no matter where the request comes from. |
| 227 | Another application could request to view a wiki note (perhaps without |
| 228 | even knowing what kind of data it is) and our activity could start up |
| 229 | and handle it.</p> |
| 230 | |
| 231 | <p>The backstack is automatically maintained for you too. As you |
| 232 | forward navigate through WikiNotes, Android maintains the history of |
| 233 | notes visited, and so when you hit the back button you go back to the |
| 234 | last note you were on. All this is free because we rely on the Android |
| 235 | intents mechanism.</p> |
| 236 | |
| 237 | <p>Finally, if you run WikiNotes for Android and then start DDMS to |
| 238 | take a look at the Activity threads in the WikiNotes application while |
| 239 | it is running, you can see that despite what you might think, letting |
| 240 | Android manage the navigation is very efficient. Create a few linked |
| 241 | notes, as many links deep as you like, and then follow them. If you |
| 242 | follow links hundreds of notes deep, you will still only see a handful |
| 243 | of WikiNotes activities. Android is managing the activities, closing |
| 244 | the older ones as necessary and using the life cycle to swap data in |
| 245 | and out.</p> |
| 246 | |
| 247 | <h3>Next Time</h3> |
| 248 | |
| 249 | <p>This was a long article, but necessarily so. It demonstrates the |
| 250 | importance of the Intents mechanism and to reinforce the notion that it |
| 251 | should be used whenever possible for forward navigation, even within a |
| 252 | single application. Illustrating this is one of the primary reasons I |
| 253 | wrote WikiNotes for Android in the first place.</p> |
| 254 | |
| 255 | <p>In the next article we will look deeper into the ContentProvider and |
| 256 | examine how it turns a Content URI into a row (or several rows) of data |
| 257 | that can be used by an activity.</p> |