Commits

Anonymous committed 7949534

Import the NFC tag app.

It came from development/apps/Tag at
f8580cf67655e5b4dcf14b2520a8897e97053608

The APK name has changed to Tag.apk.

Change-Id: I4976c4d5b656544676fdd01f64be838e4aafd30f

  • Participants
  • Parent commits 55fe86a

Comments (0)

Files changed (44)

+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_STATIC_JAVA_LIBRARIES := guava
+
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := Tag
+
+#LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
+
+# Use the following include to make our test apk.
+include $(call all-makefiles-under,$(LOCAL_PATH))

AndroidManifest.xml

+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- Declare the contents of this Android application.  The namespace
+     attribute brings in the Android platform namespace, and the package
+     supplies a unique name for the application.  When writing your
+     own application, the package name must be changed from "com.example.*"
+     to come from a domain that you own or have control over. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.apps.tag"
+>
+
+    <uses-permission android:name="android.permission.CALL_PHONE" />
+    <uses-permission android:name="android.permission.NFC" />
+
+    <application android:label="Tags">
+        <activity android:name="TagBrowserActivity"
+            android:icon="@drawable/ic_launcher_nfc"
+            android:theme="@android:style/Theme.NoTitleBar"
+        >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+
+        <activity android:name="TagList" />
+
+        <activity android:name="TagViewer"
+            android:theme="@android:style/Theme.NoTitleBar"
+        >
+            <intent-filter>
+                <action android:name="android.nfc.action.NDEF_TAG_DISCOVERED"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+
+        <service android:name="TagService" />
+
+    </application>
+</manifest>

res/drawable-hdpi/ic_launcher_nfc.png

Added
New image

res/drawable-hdpi/ic_menu_tag.png

Added
New image

res/drawable-hdpi/ic_tab_selected_starred.png

Added
New image

res/drawable-hdpi/ic_tab_unselected_starred.png

Added
New image

res/drawable-mdpi/ic_launcher_nfc.png

Added
New image

res/drawable-mdpi/ic_menu_tag.png

Added
New image

res/drawable-mdpi/ic_tab_selected_starred.png

Added
New image

res/drawable-mdpi/ic_tab_unselected_starred.png

Added
New image

res/drawable/ic_tab_starred.xml

+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true" android:state_pressed="false" android:drawable="@drawable/ic_tab_selected_starred" />
+    <item android:drawable="@drawable/ic_tab_unselected_starred" />
+</selector>
+

res/layout/main.xml

+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/tabhost"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <TabWidget
+            android:id="@android:id/tabs"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+        <FrameLayout
+            android:id="@android:id/tabcontent"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+    </LinearLayout>
+</TabHost>

res/layout/tag_divider.xml

+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?android:attr/listDivider"
+/>

res/layout/tag_list_item.xml

+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:padding="4dip"
+  android:orientation="vertical"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content">
+
+  <TextView
+    android:id="@+id/title"
+    android:padding="4dip"
+    android:textAppearance="?android:attr/textAppearanceMedium"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    />
+
+  <TextView
+    android:id="@+id/date"
+    android:padding="4dip"
+    android:textAppearance="?android:attr/textAppearanceSmall"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    />
+</LinearLayout>
+

res/layout/tag_text.xml

+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/text"
+    android:padding="4dip"
+    android:textAppearance="?android:attr/textAppearanceMedium"
+    android:layout_width="match_parent"
+    android:layout_height="?android:attr/listPreferredItemHeight"
+    android:singleLine="true"
+    android:gravity="center_vertical"
+/>

res/layout/tag_uri.xml

+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="?android:attr/listPreferredItemHeight"
+
+    android:paddingTop="4dip"
+    android:paddingBottom="4dip"
+>
+
+    <ImageView android:id="@+id/icon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentLeft="true"
+        android:layout_centerVertical="true"
+        
+        android:paddingLeft="8dip"
+        android:paddingRight="8dip"
+    />
+
+    <TextView android:id="@+id/primary"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentTop="true"
+        android:layout_toRightOf="@id/icon"
+        android:layout_marginTop="4dip"
+
+        android:textAppearance="?android:attr/textAppearanceMedium"
+    />
+
+    <TextView android:id="@+id/secondary"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/primary"
+        android:layout_alignLeft="@id/primary"
+
+        android:textAppearance="?android:attr/textAppearanceSmall"
+    />
+</RelativeLayout>
+

res/layout/tag_viewer.xml

+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+  
+          http://www.apache.org/licenses/LICENSE-2.0
+  
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+
+    android:orientation="vertical"
+>
+    <!-- Title -->
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="56dip"
+
+        android:orientation="horizontal"
+        android:background="@android:color/black"
+    >
+
+        <ImageView android:id="@+id/icon"
+            android:layout_width="32dip"
+            android:layout_height="32dip"
+            android:layout_gravity="center_vertical"
+        />
+
+        <TextView android:id="@+id/title"
+            android:layout_width="0dip"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+
+            android:singleLine="true"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+        />
+
+        <CheckBox android:id="@+id/star"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+
+            style="?android:attr/starStyle"
+        />
+
+    </LinearLayout>
+
+    <!-- Content -->
+
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1"
+
+        android:background="@android:color/white"
+    >
+
+        <LinearLayout android:id="@+id/list"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+
+            android:orientation="vertical"
+        />
+
+    </ScrollView>
+
+    <!-- Bottom button area -->
+
+    <TextView android:id="@+id/cancel_help_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+
+        android:paddingLeft="4dip"
+        android:paddingRight="4dip"
+        android:text="@string/cancel_help_text"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:background="@android:color/black"
+        android:gravity="center"
+    />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+
+        android:orientation="horizontal"
+        style="@style/ButtonBar"
+    >
+
+        <Button android:id="@+id/btn_delete"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+
+            android:text="@string/button_delete"
+        />
+
+        <Button android:id="@+id/btn_cancel"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+
+            android:text="@android:string/cancel"
+        />
+
+    </LinearLayout>
+
+</LinearLayout>

res/values/strings.xml

+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <!-- The title of the tab that displays all recently scanned NFC tags -->
+    <string name="tab_tags">Tags</string>
+
+    <!-- The title of the tab that displays all starred NFC tags -->
+    <string name="tab_starred">Starred</string>
+
+    <!-- The title displayed for unknown tag types -->
+    <string name="tag_unknown">Unknown tag type</string>
+
+    <!-- The title displayed for an empty tag -->
+    <string name="tag_empty">Empty tag</string>
+
+    <!-- Button label indicating that the user wants to delete a tag -->
+    <string name="button_delete">Delete</string>
+
+    <!-- String describing that if the user doesn't want to save a tag they should touch the button labeled Cancel. The text for the cancel button comes from the system string android.R.string.cancel. -->
+    <string name="cancel_help_text">To skip adding this tag to your collection, press Cancel</string>
+
+    <!-- String displayed for an action to send a text message to a phone number -->
+    <string name="action_text">Text <xliff:g id="phone_number">%s</xliff:g></string>
+
+    <!-- String displayed for an action to call a phone number -->
+    <string name="action_call">Call <xliff:g id="phone_number">%s</xliff:g></string>
+</resources>

res/values/styles.xml

+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <style name="ButtonBar">
+        <item name="android:paddingTop">5dip</item>
+        <item name="android:paddingLeft">4dip</item>
+        <item name="android:paddingRight">4dip</item>
+        <item name="android:paddingBottom">1dip</item>
+        <item name="android:background">@android:color/black</item>
+    </style>
+</resources>

src/com/android/apps/tag/MockNdefMessages.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.tag;
+
+/**
+ * Tags that we've seen in the field, for testing purposes.
+ */
+public class MockNdefMessages {
+
+    /**
+     * A real NFC tag containing an NFC "smart poster".  This smart poster
+     * consists of the text "NFC Forum Type 4 Tag" in english combined with
+     * the URL "http://www.nxp.com/nfc"
+     */
+    public static final byte[] REAL_NFC_MSG = new byte[] {
+            (byte) 0xd1,                   // MB=1 ME=1 CF=0 SR=1 IL=0 TNF=001
+            (byte) 0x02,                   // Type Length = 2
+            (byte) 0x2b,                   // Payload Length = 43
+            (byte) 0x53, (byte) 0x70,      // Type = {'S', 'p'} (smart poster)
+
+            // begin smart poster payload
+            // begin smart poster record #1
+            (byte) 0x91,                   // MB=1 ME=0 CF=0 SR=1 IL=0 TNF=001
+            (byte) 0x01,                   // Type Length = 1
+            (byte) 0x17,                   // Payload Length = 23
+            (byte) 0x54,                   // Type = {'T'} (Text data)
+            (byte) 0x02,                   // UTF-8 encoding, language code length = 2
+            (byte) 0x65, (byte) 0x6e,      // language = {'e', 'n'} (english)
+
+            // Begin text data within smart poster record #1
+            (byte) 0x4e,                   // 'N'
+            (byte) 0x46,                   // 'F'
+            (byte) 0x43,                   // 'C'
+            (byte) 0x20,                   // ' '
+            (byte) 0x46,                   // 'F'
+            (byte) 0x6f,                   // 'o'
+            (byte) 0x72,                   // 'r'
+            (byte) 0x75,                   // 'u'
+            (byte) 0x6d,                   // 'm'
+            (byte) 0x20,                   // ' '
+            (byte) 0x54,                   // 'T'
+            (byte) 0x79,                   // 'y'
+            (byte) 0x70,                   // 'p'
+            (byte) 0x65,                   // 'e'
+            (byte) 0x20,                   // ' '
+            (byte) 0x34,                   // '4'
+            (byte) 0x20,                   // ' '
+            (byte) 0x54,                   // 'T'
+            (byte) 0x61,                   // 'a'
+            (byte) 0x67,                   // 'g'
+            // end Text data within smart poster record #1
+            // end smart poster record #1
+
+            // begin smart poster record #2
+            (byte) 0x51,                   // MB=0 ME=1 CF=0 SR=1 IL=0 TNF=001
+            (byte) 0x01,                   // Type Length = 1
+            (byte) 0x0c,                   // Payload Length = 12
+            (byte) 0x55,                   // Type = { 'U' } (URI)
+
+            // begin URI data within smart poster record #2
+            (byte) 0x01,                   // URI Prefix = 1 ("http://www.")
+            (byte) 0x6e,                   // 'n'
+            (byte) 0x78,                   // 'x'
+            (byte) 0x70,                   // 'p'
+            (byte) 0x2e,                   // '.'
+            (byte) 0x63,                   // 'c'
+            (byte) 0x6f,                   // 'o'
+            (byte) 0x6d,                   // 'm'
+            (byte) 0x2f,                   // '/'
+            (byte) 0x6e,                   // 'n'
+            (byte) 0x66,                   // 'f'
+            (byte) 0x63                    // 'c'
+            // end URI data within smart poster record #2
+            // end smart poster record #2
+            // end smart poster payload
+    };
+
+
+    /**
+     * A Smart Poster containing a URL and no text.  This message was created
+     * using the NXP reference phone.
+     */
+    private static final byte[] SMART_POSTER_URL_NO_TEXT = new byte[] {
+            (byte) 0xd1, (byte) 0x02, (byte) 0x0f, (byte) 0x53, (byte) 0x70, (byte) 0xd1,
+            (byte) 0x01, (byte) 0x0b, (byte) 0x55, (byte) 0x01, (byte) 0x67, (byte) 0x6f,
+            (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, (byte) 0x2e, (byte) 0x63,
+            (byte) 0x6f, (byte) 0x6d
+    };
+
+    /**
+     * A plain text tag in english.  Generated using the NXP evaluation tool.
+     */
+    private static final byte[] ENGLISH_PLAIN_TEXT = new byte[] {
+            (byte) 0xd1, (byte) 0x01, (byte) 0x1c, (byte) 0x54, (byte) 0x02, (byte) 0x65,
+            (byte) 0x6e, (byte) 0x53, (byte) 0x6f, (byte) 0x6d, (byte) 0x65, (byte) 0x20,
+            (byte) 0x72, (byte) 0x61, (byte) 0x6e, (byte) 0x64, (byte) 0x6f, (byte) 0x6d,
+            (byte) 0x20, (byte) 0x65, (byte) 0x6e, (byte) 0x67, (byte) 0x6c, (byte) 0x69,
+            (byte) 0x73, (byte) 0x68, (byte) 0x20, (byte) 0x74, (byte) 0x65, (byte) 0x78,
+            (byte) 0x74, (byte) 0x2e
+    };
+
+    /**
+     * Smart Poster containing a URL and Text.  Generated using the NXP
+     * evaluation tool.
+     */
+    private static final byte[] SMART_POSTER_URL_AND_TEXT = new byte[] {
+            (byte) 0xd1, (byte) 0x02, (byte) 0x1c, (byte) 0x53, (byte) 0x70, (byte) 0x91,
+            (byte) 0x01, (byte) 0x09, (byte) 0x54, (byte) 0x02, (byte) 0x65, (byte) 0x6e,
+            (byte) 0x47, (byte) 0x6f, (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65,
+            (byte) 0x51, (byte) 0x01, (byte) 0x0b, (byte) 0x55, (byte) 0x01, (byte) 0x67,
+            (byte) 0x6f, (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, (byte) 0x2e,
+            (byte) 0x63, (byte) 0x6f, (byte) 0x6d
+    };
+
+    /**
+     * A plain URI.  Generated using the NXP evaluation tool.
+     */
+    private static final byte[] URI = new byte[] {
+            (byte) 0xd1, (byte) 0x01, (byte) 0x0b, (byte) 0x55, (byte) 0x01, (byte) 0x67,
+            (byte) 0x6f, (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, (byte) 0x2e,
+            (byte) 0x63, (byte) 0x6f, (byte) 0x6d
+    };
+
+    /**
+     * A vcard.  Generated using the NXP evaluation tool.
+     */
+    private static final byte[] VCARD = new byte[] {
+            (byte) 0xc2, (byte) 0x0c, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x05,
+            (byte) 0x74, (byte) 0x65, (byte) 0x78, (byte) 0x74, (byte) 0x2f, (byte) 0x78,
+            (byte) 0x2d, (byte) 0x76, (byte) 0x43, (byte) 0x61, (byte) 0x72, (byte) 0x64,
+            (byte) 0x42, (byte) 0x45, (byte) 0x47, (byte) 0x49, (byte) 0x4e, (byte) 0x3a,
+            (byte) 0x56, (byte) 0x43, (byte) 0x41, (byte) 0x52, (byte) 0x44, (byte) 0x0d,
+            (byte) 0x0a, (byte) 0x56, (byte) 0x45, (byte) 0x52, (byte) 0x53, (byte) 0x49,
+            (byte) 0x4f, (byte) 0x4e, (byte) 0x3a, (byte) 0x33, (byte) 0x2e, (byte) 0x30,
+            (byte) 0x0d, (byte) 0x0a, (byte) 0x46, (byte) 0x4e, (byte) 0x3a, (byte) 0x4a,
+            (byte) 0x6f, (byte) 0x65, (byte) 0x20, (byte) 0x47, (byte) 0x6f, (byte) 0x6f,
+            (byte) 0x67, (byte) 0x6c, (byte) 0x65, (byte) 0x20, (byte) 0x45, (byte) 0x6d,
+            (byte) 0x70, (byte) 0x6c, (byte) 0x6f, (byte) 0x79, (byte) 0x65, (byte) 0x65,
+            (byte) 0x0d, (byte) 0x0a, (byte) 0x41, (byte) 0x44, (byte) 0x52, (byte) 0x3b,
+            (byte) 0x54, (byte) 0x59, (byte) 0x50, (byte) 0x45, (byte) 0x3d, (byte) 0x57,
+            (byte) 0x4f, (byte) 0x52, (byte) 0x4b, (byte) 0x3a, (byte) 0x3b, (byte) 0x3b,
+            (byte) 0x31, (byte) 0x36, (byte) 0x30, (byte) 0x30, (byte) 0x20, (byte) 0x41,
+            (byte) 0x6d, (byte) 0x70, (byte) 0x68, (byte) 0x69, (byte) 0x74, (byte) 0x68,
+            (byte) 0x65, (byte) 0x61, (byte) 0x74, (byte) 0x72, (byte) 0x65, (byte) 0x20,
+            (byte) 0x50, (byte) 0x61, (byte) 0x72, (byte) 0x6b, (byte) 0x77, (byte) 0x61,
+            (byte) 0x79, (byte) 0x3b, (byte) 0x39, (byte) 0x34, (byte) 0x30, (byte) 0x34,
+            (byte) 0x33, (byte) 0x20, (byte) 0x4d, (byte) 0x6f, (byte) 0x75, (byte) 0x6e,
+            (byte) 0x74, (byte) 0x61, (byte) 0x69, (byte) 0x6e, (byte) 0x20, (byte) 0x56,
+            (byte) 0x69, (byte) 0x65, (byte) 0x77, (byte) 0x0d, (byte) 0x0a, (byte) 0x54,
+            (byte) 0x45, (byte) 0x4c, (byte) 0x3b, (byte) 0x54, (byte) 0x59, (byte) 0x50,
+            (byte) 0x45, (byte) 0x3d, (byte) 0x50, (byte) 0x52, (byte) 0x45, (byte) 0x46,
+            (byte) 0x2c, (byte) 0x57, (byte) 0x4f, (byte) 0x52, (byte) 0x4b, (byte) 0x3a,
+            (byte) 0x36, (byte) 0x35, (byte) 0x30, (byte) 0x2d, (byte) 0x32, (byte) 0x35,
+            (byte) 0x33, (byte) 0x2d, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30,
+            (byte) 0x0d, (byte) 0x0a, (byte) 0x45, (byte) 0x4d, (byte) 0x41, (byte) 0x49,
+            (byte) 0x4c, (byte) 0x3b, (byte) 0x54, (byte) 0x59, (byte) 0x50, (byte) 0x45,
+            (byte) 0x3d, (byte) 0x49, (byte) 0x4e, (byte) 0x54, (byte) 0x45, (byte) 0x52,
+            (byte) 0x4e, (byte) 0x45, (byte) 0x54, (byte) 0x3a, (byte) 0x73, (byte) 0x75,
+            (byte) 0x70, (byte) 0x70, (byte) 0x6f, (byte) 0x72, (byte) 0x74, (byte) 0x40,
+            (byte) 0x67, (byte) 0x6f, (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65,
+            (byte) 0x2e, (byte) 0x63, (byte) 0x6f, (byte) 0x6d, (byte) 0x0d, (byte) 0x0a,
+            (byte) 0x54, (byte) 0x49, (byte) 0x54, (byte) 0x4c, (byte) 0x45, (byte) 0x3a,
+            (byte) 0x53, (byte) 0x6f, (byte) 0x66, (byte) 0x74, (byte) 0x77, (byte) 0x61,
+            (byte) 0x72, (byte) 0x65, (byte) 0x20, (byte) 0x45, (byte) 0x6e, (byte) 0x67,
+            (byte) 0x69, (byte) 0x6e, (byte) 0x65, (byte) 0x65, (byte) 0x72, (byte) 0x0d,
+            (byte) 0x0a, (byte) 0x4f, (byte) 0x52, (byte) 0x47, (byte) 0x3a, (byte) 0x47,
+            (byte) 0x6f, (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, (byte) 0x0d,
+            (byte) 0x0a, (byte) 0x55, (byte) 0x52, (byte) 0x4c, (byte) 0x3a, (byte) 0x68,
+            (byte) 0x74, (byte) 0x74, (byte) 0x70, (byte) 0x3a, (byte) 0x2f, (byte) 0x2f,
+            (byte) 0x77, (byte) 0x77, (byte) 0x77, (byte) 0x2e, (byte) 0x67, (byte) 0x6f,
+            (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, (byte) 0x2e, (byte) 0x63,
+            (byte) 0x6f, (byte) 0x6d, (byte) 0x0d, (byte) 0x0a, (byte) 0x45, (byte) 0x4e,
+            (byte) 0x44, (byte) 0x3a, (byte) 0x56, (byte) 0x43, (byte) 0x41, (byte) 0x52,
+            (byte) 0x44, (byte) 0x0d, (byte) 0x0a
+    };
+
+    /**
+     * Send the text message "hello world" to a phone number.  This was generated using
+     * the NXP reference phone.
+     */
+    private static final byte[] SEND_TEXT_MESSAGE = new byte[] {
+            (byte) 0xd1, (byte) 0x02, (byte) 0x25, (byte) 0x53, (byte) 0x70, (byte) 0xd1,
+            (byte) 0x01, (byte) 0x21, (byte) 0x55, (byte) 0x00, (byte) 0x73, (byte) 0x6d,
+            (byte) 0x73, (byte) 0x3a, (byte) 0x31, (byte) 0x36, (byte) 0x35, (byte) 0x30,
+            (byte) 0x32, (byte) 0x35, (byte) 0x33, (byte) 0x30, (byte) 0x30, (byte) 0x30,
+            (byte) 0x30, (byte) 0x3f, (byte) 0x62, (byte) 0x6f, (byte) 0x64, (byte) 0x79,
+            (byte) 0x3d, (byte) 0x48, (byte) 0x65, (byte) 0x6c, (byte) 0x6c, (byte) 0x6f,
+            (byte) 0x20, (byte) 0x77, (byte) 0x6f, (byte) 0x72, (byte) 0x6c, (byte) 0x64
+    };
+
+    /**
+     * Call Google.  Generated using the NXP reference phone.
+     */
+    private static final byte[] CALL_GOOGLE = new byte[] {
+            (byte) 0xd1, (byte) 0x02, (byte) 0x10, (byte) 0x53, (byte) 0x70, (byte) 0xd1,
+            (byte) 0x01, (byte) 0x0c, (byte) 0x55, (byte) 0x05, (byte) 0x31, (byte) 0x36,
+            (byte) 0x35, (byte) 0x30, (byte) 0x32, (byte) 0x35, (byte) 0x33, (byte) 0x30,
+            (byte) 0x30, (byte) 0x30, (byte) 0x30
+    };
+
+    /**
+     * All the real ndef messages we've seen in the field.
+     */
+    public static final byte[][] ALL_MOCK_MESSAGES = new byte[][] {
+            REAL_NFC_MSG, SMART_POSTER_URL_NO_TEXT, ENGLISH_PLAIN_TEXT,
+            SMART_POSTER_URL_AND_TEXT, URI, VCARD, SEND_TEXT_MESSAGE,
+            CALL_GOOGLE
+    };
+
+}

src/com/android/apps/tag/NdefUtil.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.tag;
+
+import android.net.Uri;
+import android.nfc.NdefRecord;
+
+import com.google.common.primitives.Bytes;
+
+import java.nio.charset.Charsets;
+
+/**
+ * Utilities for dealing with conversions to and from NdefRecords.
+ *
+ * TODO: Possibly move this class into core Android.
+ */
+public class NdefUtil {
+    private static final byte[] EMPTY = new byte[0];
+
+    /**
+     * Create a new {@link NdefRecord} containing the supplied {@link Uri}.
+     */
+    public static NdefRecord toUriRecord(Uri uri) {
+        byte[] uriBytes = uri.toString().getBytes(Charsets.UTF_8);
+
+        /*
+         * We prepend 0x00 to the bytes of the URI to indicate that this
+         * is the entire URI, and we are not taking advantage of the
+         * URI shortening rules in the NFC Forum URI spec section 3.2.2.
+         * This produces a NdefRecord which is slightly larger than
+         * necessary.
+         *
+         * In the future, we should use the URI shortening rules in 3.2.2
+         * to create a smaller NdefRecord.
+         */
+        byte[] payload = Bytes.concat(new byte[] { 0x00 }, uriBytes);
+
+        return new NdefRecord(NdefRecord.TNF_WELL_KNOWN,
+                NdefRecord.RTD_URI, EMPTY, payload);
+    }
+}

src/com/android/apps/tag/TagAdapter.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.tag;
+
+import com.android.apps.tag.TagDBHelper.NdefMessagesTable;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Adapter;
+import android.widget.CursorAdapter;
+import android.widget.TextView;
+
+/**
+ * A custom {@link Adapter} that renders tag entries for a list.
+ */
+public class TagAdapter extends CursorAdapter {
+
+    private final LayoutInflater mInflater;
+
+    public TagAdapter(Context context) {
+        super(context, null, false);
+        mInflater = LayoutInflater.from(context);
+    }
+
+    @Override
+    public void bindView(View view, Context context, Cursor cursor) {
+        TextView mainLine = (TextView) view.findViewById(R.id.title);
+        TextView dateLine = (TextView) view.findViewById(R.id.date);
+
+        mainLine.setText(cursor.getString(cursor.getColumnIndex(NdefMessagesTable.TITLE)));
+        dateLine.setText(DateUtils.getRelativeTimeSpanString(
+                context, cursor.getLong(cursor.getColumnIndex(NdefMessagesTable.DATE))));
+    }
+
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        return mInflater.inflate(R.layout.tag_list_item, null);
+    }
+}

src/com/android/apps/tag/TagBrowserActivity.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.tag;
+
+import android.app.Activity;
+import android.app.TabActivity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.widget.TabHost;
+
+/**
+ * A browsing {@link Activity} that displays the saved tags in categories under tabs.
+ */
+public class TagBrowserActivity extends TabActivity {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.main);
+
+        Resources res = getResources();
+        TabHost tabHost = getTabHost();
+
+        tabHost.addTab(tabHost.newTabSpec("tags")
+                .setIndicator(getText(R.string.tab_tags),
+                        res.getDrawable(R.drawable.ic_menu_tag))
+                .setContent(new Intent().setClass(this, TagList.class)));
+
+        tabHost.addTab(tabHost.newTabSpec("starred")
+                .setIndicator(getText(R.string.tab_starred),
+                        res.getDrawable(R.drawable.ic_tab_starred))
+                .setContent(new Intent().setClass(this, TagList.class)
+                        .putExtra(TagList.EXTRA_SHOW_STARRED_ONLY, true)));
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        // Restore the last active tab
+        SharedPreferences prefs = getSharedPreferences("prefs", Context.MODE_PRIVATE);
+        getTabHost().setCurrentTabByTag(prefs.getString("tab", "tags"));
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+
+        // Save the active tab
+        SharedPreferences.Editor edit = getSharedPreferences("prefs", Context.MODE_PRIVATE).edit();
+        edit.putString("tab", getTabHost().getCurrentTabTag());
+        edit.apply();
+    }
+}

src/com/android/apps/tag/TagDBHelper.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.tag;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteStatement;
+import android.net.Uri;
+import android.nfc.FormatException;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+
+import java.util.Locale;
+
+import com.android.apps.tag.message.NdefMessageParser;
+import com.android.apps.tag.message.ParsedNdefMessage;
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * Database utilities for the saved tags.
+ */
+public class TagDBHelper extends SQLiteOpenHelper {
+
+    private static final String DATABASE_NAME = "tags.db";
+    private static final int DATABASE_VERSION = 5;
+
+    public interface NdefMessagesTable {
+        public static final String TABLE_NAME = "nedf_msg";
+
+        public static final String _ID = "_id";
+        public static final String TITLE = "title";
+        public static final String BYTES = "bytes";
+        public static final String DATE = "date";
+        public static final String STARRED = "starred";
+    }
+
+    private static TagDBHelper sInstance;
+
+    private Context mContext;
+
+    public static synchronized TagDBHelper getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new TagDBHelper(context.getApplicationContext());
+        }
+        return sInstance;
+    }
+
+    private TagDBHelper(Context context) {
+        this(context, DATABASE_NAME);
+        mContext = context;
+    }
+
+    @VisibleForTesting
+    TagDBHelper(Context context, String dbFile) {
+        super(context, dbFile, null, DATABASE_VERSION);
+        mContext = context;
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE " + NdefMessagesTable.TABLE_NAME + " (" +
+                NdefMessagesTable._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                NdefMessagesTable.TITLE + " TEXT NOT NULL DEFAULT ''," +
+                NdefMessagesTable.BYTES + " BLOB NOT NULL, " +
+                NdefMessagesTable.DATE + " INTEGER NOT NULL, " +
+                NdefMessagesTable.STARRED + " INTEGER NOT NULL DEFAULT 0" +  // boolean
+                ");");
+
+        db.execSQL("CREATE INDEX msgIndex ON " + NdefMessagesTable.TABLE_NAME + " (" +
+                NdefMessagesTable.DATE + " DESC, " +
+                NdefMessagesTable.STARRED + " ASC" +
+                ")");
+
+        addTestData(db);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        // Drop everything and recreate it for now
+        db.execSQL("DROP TABLE IF EXISTS " + NdefMessagesTable.TABLE_NAME);
+        onCreate(db);
+    }
+
+    private void addTestData(SQLiteDatabase db) {
+        // A fake message containing 1 URL
+        NdefMessage msg1 = new NdefMessage(new NdefRecord[] {
+                NdefUtil.toUriRecord(Uri.parse("http://www.google.com"))
+        });
+
+        // A fake message containing 2 URLs
+        NdefMessage msg2 = new NdefMessage(new NdefRecord[] {
+                NdefUtil.toUriRecord(Uri.parse("http://www.youtube.com")),
+                NdefUtil.toUriRecord(Uri.parse("http://www.android.com"))
+        });
+
+        insertNdefMessage(db, msg1, false);
+        insertNdefMessage(db, msg2, true);
+
+        try {
+            // insert some real messages we found in the field.
+            for (byte[] msg : MockNdefMessages.ALL_MOCK_MESSAGES) {
+                NdefMessage msg3 = new NdefMessage(msg);
+                insertNdefMessage(db, msg3, false);
+            }
+        } catch (FormatException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void insertNdefMessage(SQLiteDatabase db, NdefMessage msg, boolean isStarred) {
+        ParsedNdefMessage parsedMsg = NdefMessageParser.parse(msg);
+        SQLiteStatement stmt = null;
+        try {
+            stmt = db.compileStatement("INSERT INTO " + NdefMessagesTable.TABLE_NAME +
+                    "(" + NdefMessagesTable.BYTES + ", " + NdefMessagesTable.DATE + ", " +
+                    NdefMessagesTable.STARRED + "," + NdefMessagesTable.TITLE + ") " +
+                    "values (?, ?, ?, ?)");
+            stmt.bindBlob(1, msg.toByteArray());
+            stmt.bindLong(2, System.currentTimeMillis());
+            stmt.bindLong(3, isStarred ? 1 : 0);
+            stmt.bindString(4, parsedMsg.getSnippet(mContext, Locale.getDefault()));
+            stmt.executeInsert();
+        } finally {
+            if (stmt != null) stmt.close();
+        }
+    }
+}

src/com/android/apps/tag/TagList.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.tag;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ListActivity;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.nfc.FormatException;
+import android.nfc.NdefMessage;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.View;
+import android.widget.ListView;
+
+import com.android.apps.tag.TagDBHelper.NdefMessagesTable;
+
+/**
+ * An {@link Activity} that displays a flat list of tags that can be "opened".
+ */
+public class TagList extends ListActivity implements DialogInterface.OnClickListener {
+    static final String TAG = "TagList";
+
+    static final String EXTRA_SHOW_STARRED_ONLY = "show_starred_only";
+
+    SQLiteDatabase mDatabase;
+    TagAdapter mAdapter;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        boolean showStarredOnly = getIntent().getBooleanExtra(EXTRA_SHOW_STARRED_ONLY, false);
+        mDatabase = TagDBHelper.getInstance(this).getReadableDatabase();
+        String selection = showStarredOnly ? NdefMessagesTable.STARRED + "=1" : null;
+
+        new TagLoaderTask().execute(selection);
+        mAdapter = new TagAdapter(this);
+        setListAdapter(mAdapter);
+        registerForContextMenu(getListView());
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        menu.add("hello world");
+        return true;
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int id, Bundle args) {
+        String[] stuff = new String[] { "a", "b" };
+        return new AlertDialog.Builder(this)
+                .setTitle("blah")
+                .setItems(stuff, this)
+                .setPositiveButton("Delete", null)
+                .setNegativeButton("Cancel", null)
+                .create();
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (mAdapter != null) {
+            mAdapter.changeCursor(null);
+        }
+        super.onDestroy();
+    }
+
+    @Override
+    protected void onListItemClick(ListView l, View v, int position, long id) {
+        Cursor cursor = mAdapter.getCursor();
+        cursor.moveToPosition(position);
+        byte[] tagBytes = cursor.getBlob(cursor.getColumnIndexOrThrow(NdefMessagesTable.BYTES));
+        try {
+            NdefMessage msg = new NdefMessage(tagBytes);
+            Intent intent = new Intent(this, TagViewer.class);
+            intent.putExtra(TagViewer.EXTRA_MESSAGE, msg);
+            intent.putExtra(TagViewer.EXTRA_TAG_DB_ID, id);
+            startActivity(intent);
+        } catch (FormatException e) {
+            Log.e(TAG, "bad format for tag " + id + ": " + tagBytes, e);
+            return;
+        }
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+    }
+
+    final class TagLoaderTask extends AsyncTask<String, Void, Cursor> {
+        @Override
+        public Cursor doInBackground(String... args) {
+            String selection = args[0];
+            Cursor cursor = mDatabase.query(
+                    NdefMessagesTable.TABLE_NAME,
+                    new String[] { 
+                            NdefMessagesTable._ID,
+                            NdefMessagesTable.BYTES,
+                            NdefMessagesTable.DATE,
+                            NdefMessagesTable.TITLE },
+                    selection,
+                    null, null, null, NdefMessagesTable.DATE + " DESC");
+            cursor.getCount();
+            return cursor;
+        }
+
+        @Override
+        protected void onPostExecute(Cursor cursor) {
+            mAdapter.changeCursor(cursor);
+        }
+    }
+}

src/com/android/apps/tag/TagService.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.apps.tag;
+
+import com.android.apps.tag.TagDBHelper.NdefMessagesTable;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.database.sqlite.SQLiteDatabase;
+import android.nfc.NdefMessage;
+import android.os.Parcelable;
+
+public class TagService extends IntentService {
+    public static final String EXTRA_SAVE_MSGS = "msgs";
+    public static final String EXTRA_DELETE_ID = "delete";
+
+    public TagService() {
+        super("SaveTagService");
+    }
+
+    @Override
+    public void onHandleIntent(Intent intent) {
+        TagDBHelper helper = TagDBHelper.getInstance(this);
+        SQLiteDatabase db = helper.getWritableDatabase();
+        if (intent.hasExtra(EXTRA_SAVE_MSGS)) {
+            Parcelable[] parcels = intent.getParcelableArrayExtra(EXTRA_SAVE_MSGS);
+            db.beginTransaction();
+            try {
+                for (Parcelable parcel : parcels) {
+                    helper.insertNdefMessage(db, (NdefMessage) parcel, false);
+                }
+                db.setTransactionSuccessful();
+            } finally {
+                db.endTransaction();
+            }
+            return;
+        } else if (intent.hasExtra(EXTRA_DELETE_ID)) {
+            long id = intent.getLongExtra(EXTRA_DELETE_ID, 0);
+            db.delete(NdefMessagesTable.TABLE_NAME, NdefMessagesTable._ID + "=?",
+                    new String[] { Long.toString(id) });
+            return;
+        }
+    }
+}

src/com/android/apps/tag/TagViewer.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.tag;
+
+import com.android.apps.tag.message.NdefMessageParser;
+import com.android.apps.tag.message.ParsedNdefMessage;
+import com.android.apps.tag.record.ParsedNdefRecord;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.nfc.NdefMessage;
+import android.nfc.NdefTag;
+import android.nfc.NfcAdapter;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.util.Locale;
+
+/**
+ * An {@link Activity} which handles a broadcast of a new tag that the device just discovered.
+ */
+public class TagViewer extends Activity implements OnClickListener, Handler.Callback {
+    static final String TAG = "SaveTag";    
+    static final String EXTRA_TAG_DB_ID = "db_id";
+    static final String EXTRA_MESSAGE = "msg";
+
+    /** This activity will finish itself in this amount of time if the user doesn't do anything. */
+    static final int ACTIVITY_TIMEOUT_MS = 10 * 1000;
+
+    long mTagDatabaseId;
+    ImageView mIcon;
+    TextView mTitle;
+    CheckBox mStar;
+    Button mDeleteButton;
+    Button mCancelButton;
+    NdefMessage[] mMessagesToSave = null;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+                | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_DIM_BEHIND
+        );
+
+        setContentView(R.layout.tag_viewer);
+
+        mTitle = (TextView) findViewById(R.id.title);
+        mIcon = (ImageView) findViewById(R.id.icon);
+        mStar = (CheckBox) findViewById(R.id.star);
+        mDeleteButton = (Button) findViewById(R.id.btn_delete);
+        mCancelButton = (Button) findViewById(R.id.btn_cancel);
+
+        mDeleteButton.setOnClickListener(this);
+        mCancelButton.setOnClickListener(this);
+        mIcon.setImageResource(R.drawable.ic_launcher_nfc);
+
+        Intent intent = getIntent();
+        NdefMessage[] msgs = null;
+        NdefTag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
+        if (tag == null) {
+            // Maybe it came from the database? 
+            mTagDatabaseId = intent.getLongExtra(EXTRA_TAG_DB_ID, -1);
+            NdefMessage msg = intent.getParcelableExtra(EXTRA_MESSAGE);
+            if (msg != null) {
+                msgs = new NdefMessage[] { msg };
+            }
+
+            // Hide the text about saving the tag, it's already in the database
+            findViewById(R.id.cancel_help_text).setVisibility(View.GONE);
+        } else {
+            msgs = tag.getNdefMessages();
+            mDeleteButton.setVisibility(View.GONE);
+
+            // Set a timer on this activity since it wasn't created by the user
+            new Handler(this).sendEmptyMessageDelayed(0, ACTIVITY_TIMEOUT_MS);
+            
+            // Save the messages that were just scanned
+            mMessagesToSave = msgs;
+        }
+
+        if (msgs == null || msgs.length == 0) {
+            Log.e(TAG, "No NDEF messages");
+            finish();
+            return;
+        }
+
+        Context contentContext = new ContextThemeWrapper(this, android.R.style.Theme_Light); 
+        LayoutInflater inflater = LayoutInflater.from(contentContext);
+        LinearLayout list = (LinearLayout) findViewById(R.id.list);
+
+        buildTagViews(list, inflater, msgs);
+
+        if (TextUtils.isEmpty(getTitle())) {
+            // There isn't a snippet for this tag, use a default title
+            setTitle(R.string.tag_unknown);
+        }
+    }
+
+    private void buildTagViews(LinearLayout list, LayoutInflater inflater, NdefMessage[] msgs) {
+        if (msgs == null || msgs.length == 0) {
+            return;
+        }
+
+        // Build the views from the logical records in the messages
+        NdefMessage msg = msgs[0];
+
+        // Set the title to be the snippet of the message
+        ParsedNdefMessage parsedMsg = NdefMessageParser.parse(msg);
+        setTitle(parsedMsg.getSnippet(this, Locale.getDefault()));
+
+        // Build views for all of the sub records
+        for (ParsedNdefRecord record : parsedMsg.getRecords()) {
+            list.addView(record.getView(this, inflater, list));
+            inflater.inflate(R.layout.tag_divider, list, true);
+        }
+    }
+
+    @Override
+    public void setTitle(CharSequence title) {
+        mTitle.setText(title);
+    }
+
+    @Override
+    public void onClick(View view) {
+        if (view == mDeleteButton) {
+            Intent save = new Intent(this, TagService.class);
+            save.putExtra(TagService.EXTRA_DELETE_ID, mTagDatabaseId);
+            startService(save);
+            finish();
+        } else if (view == mCancelButton) {
+            mMessagesToSave = null;
+            finish();
+        }
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        if (mMessagesToSave != null) {
+            saveMessages(mMessagesToSave);
+        }
+    }
+
+    void saveMessages(NdefMessage[] msgs) {
+        Intent save = new Intent(this, TagService.class);
+        save.putExtra(TagService.EXTRA_SAVE_MSGS, msgs);
+        startService(save);
+    }
+
+    @Override
+    public boolean handleMessage(Message msg) {
+        finish();
+        return true;
+    }
+}

src/com/android/apps/tag/message/EmptyMessage.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.tag.message;
+
+import com.android.apps.tag.R;
+import com.android.apps.tag.record.ParsedNdefRecord;
+
+import android.content.Context;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+/**
+ * A parsed message containing no elements.
+ */
+class EmptyMessage extends ParsedNdefMessage {
+
+    /* package private */ EmptyMessage() {
+        super(new ArrayList<ParsedNdefRecord>());
+    }
+
+    @Override
+    public String getSnippet(Context context, Locale locale) {
+        return context.getString(R.string.tag_empty);
+    }
+
+    @Override
+    public boolean isStarred() {
+        return false;
+    }
+}

src/com/android/apps/tag/message/NdefMessageParser.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.tag.message;
+
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+
+import com.android.apps.tag.record.ParsedNdefRecord;
+import com.android.apps.tag.record.SmartPoster;
+import com.android.apps.tag.record.TextRecord;
+import com.android.apps.tag.record.UriRecord;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class for creating {@link ParsedNdefMessage}s.
+ */
+public class NdefMessageParser {
+
+    // Utility class
+    private NdefMessageParser() { }
+
+    /** Parse an NdefMessage */
+    public static ParsedNdefMessage parse(NdefMessage message) {
+        List<ParsedNdefRecord> elements = getRecords(message);
+
+        if (elements.isEmpty()) {
+            return new EmptyMessage();
+        }
+
+        ParsedNdefRecord first = elements.get(0);
+
+        if (elements.size() == 1) {
+            if (first instanceof SmartPoster) {
+                return new SmartPosterMessage((SmartPoster) first, elements);
+            }
+            if (first instanceof TextRecord) {
+                return new TextMessage((TextRecord) first, elements);
+            }
+            if (first instanceof UriRecord) {
+                return new UriMessage((UriRecord) first, elements);
+            }
+        }
+
+        return new UnknownMessage(elements);
+    }
+
+    public static List<ParsedNdefRecord> getRecords(NdefMessage message) {
+        List<ParsedNdefRecord> elements = new ArrayList<ParsedNdefRecord>();
+        for (NdefRecord record : message.getRecords()) {
+            if (UriRecord.isUri(record)) {
+                elements.add(UriRecord.parse(record));
+            } else if (TextRecord.isText(record)) {
+                elements.add(TextRecord.parse(record));
+            } else if (SmartPoster.isPoster(record)) {
+                elements.add(SmartPoster.parse(record));
+            }
+        }
+        return elements;
+    }
+}

src/com/android/apps/tag/message/ParsedNdefMessage.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.tag.message;
+
+import com.android.apps.tag.record.ParsedNdefRecord;
+import com.google.common.collect.ImmutableList;
+
+import android.content.Context;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A parsed version of an {@link android.nfc.NdefMessage}
+ */
+public abstract class ParsedNdefMessage {
+
+    private List<ParsedNdefRecord> mRecords;
+
+    public ParsedNdefMessage(List<ParsedNdefRecord> records) {
+        mRecords = ImmutableList.copyOf(records);
+    }
+
+    /**
+     * Returns the list of parsed records on this message.
+     */
+    public List<ParsedNdefRecord> getRecords() {
+        return mRecords;
+    }
+
+    /**
+     * Returns the snippet information associated with the NdefMessage
+     * most appropriate for the given {@code locale}.
+     */
+    public abstract String getSnippet(Context context, Locale locale);
+
+    // TODO: Determine if this is the best place for holding whether
+    // the user has starred this parsed message.
+    public abstract boolean isStarred();
+}

src/com/android/apps/tag/message/SmartPosterMessage.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.tag.message;
+
+import com.android.apps.tag.record.ParsedNdefRecord;
+import com.android.apps.tag.record.SmartPoster;
+import com.android.apps.tag.record.TextRecord;
+import com.google.common.base.Preconditions;
+
+import android.content.Context;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A message consisting of one {@link SmartPoster} object.
+ */
+class SmartPosterMessage extends ParsedNdefMessage {
+    private final SmartPoster mPoster;
+
+    SmartPosterMessage(SmartPoster poster, List<ParsedNdefRecord> records) {
+        super(Preconditions.checkNotNull(records));
+        mPoster = Preconditions.checkNotNull(poster);
+    }
+
+    @Override
+    public String getSnippet(Context context, Locale locale) {
+        TextRecord title = mPoster.getTitle();
+        if (title == null) {
+            return mPoster.getUriRecord().getPrettyUriString(context);
+        }
+        return title.getText();
+    }
+
+    @Override
+    public boolean isStarred() {
+        return false;
+    }
+}

src/com/android/apps/tag/message/TextMessage.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.tag.message;
+
+import com.android.apps.tag.record.ParsedNdefRecord;
+import com.android.apps.tag.record.TextRecord;
+import com.google.common.base.Preconditions;
+
+import android.content.Context;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A message containing one text element
+ */
+class TextMessage extends ParsedNdefMessage {
+    private final TextRecord mRecord;
+
+    TextMessage(TextRecord record, List<ParsedNdefRecord> records) {
+        super(Preconditions.checkNotNull(records));
+        mRecord = Preconditions.checkNotNull(record);
+    }
+
+    @Override
+    public String getSnippet(Context context, Locale locale) {
+        return mRecord.getText();
+    }
+
+    @Override
+    public boolean isStarred() {
+        return false;
+    }
+}

src/com/android/apps/tag/message/UnknownMessage.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.tag.message;
+
+import com.android.apps.tag.R;
+import com.android.apps.tag.record.ParsedNdefRecord;
+import com.google.common.base.Preconditions;
+
+import android.content.Context;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * The catchall parsed message format for when nothing else better applies.
+ */
+class UnknownMessage extends ParsedNdefMessage {
+
+    UnknownMessage(List<ParsedNdefRecord> records) {
+        super(Preconditions.checkNotNull(records));
+    }
+
+    @Override
+    public String getSnippet(Context context, Locale locale) {
+        return context.getString(R.string.tag_unknown);
+    }
+
+    @Override
+    public boolean isStarred() {
+        return false;
+    }
+}

src/com/android/apps/tag/message/UriMessage.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.tag.message;
+
+import com.android.apps.tag.record.ParsedNdefRecord;
+import com.android.apps.tag.record.UriRecord;
+import com.google.common.base.Preconditions;
+
+import android.content.Context;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A {@link ParsedNdefMessage} consisting of one {@link UriRecord}.
+ */
+class UriMessage extends ParsedNdefMessage {
+
+    private final UriRecord mRecord;
+
+    UriMessage(UriRecord record, List<ParsedNdefRecord> records) {
+        super(Preconditions.checkNotNull(records));
+        mRecord = Preconditions.checkNotNull(record);
+    }
+
+    @Override
+    public String getSnippet(Context context, Locale locale) {
+        // URIs cannot be localized
+        return mRecord.getPrettyUriString(context);
+    }
+
+    @Override
+    public boolean isStarred() {
+        return false;
+    }
+}

src/com/android/apps/tag/record/ParsedNdefRecord.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apps.tag.record;
+
+import android.app.Activity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * TODO: come up with a better name.
+ */
+public interface ParsedNdefRecord {
+
+    // Just a placeholder for now.  Probably not needed nor desired.
+    public String getRecordType();
+
+    /**
+     * Returns a view to display this record.
+     */
+    public View getView(Activity activity, LayoutInflater inflater, ViewGroup parent);
+}

src/com/android/apps/tag/record/SmartPoster.java

+/*