Checkpoint work on the Tag app.

Change-Id: I375aeabe00528d30ce49e8d3d9f78aaac10b0071
diff --git a/apps/Tag/AndroidManifest.xml b/apps/Tag/AndroidManifest.xml
index 4414d73..ba01fff 100644
--- a/apps/Tag/AndroidManifest.xml
+++ b/apps/Tag/AndroidManifest.xml
@@ -20,26 +20,33 @@
      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">
+    package="com.android.apps.tag"
+>
+
+    <uses-permission android:name="com.trustedlogic.trustednfc.permission.NFC_NOTIFY" />
+    <uses-permission android:name="com.trustedlogic.trustednfc.permission.NFC_RAW" />
+
     <application android:label="Tags">
-      <activity android:name="TagBrowserActivity"
-              android:icon="@drawable/ic_launcher_nfc"
-              android:theme="@style/Tags.TagBrowserTheme" >
+        <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>
-        <activity android:name="SaveTag"></activity>
-        <receiver android:name=".TagBroadcastReceiver">
+        <activity android:name="TagList" />
+
+        <activity android:name="TagViewer"
+            android:theme="@android:style/Theme.Dialog"
+        >
             <intent-filter>
-                <action android:name= "com.trustedlogic.trustednfc.android.action.NDEF_TAG_DISCOVERED"/>
+                <action android:name="android.nfc.action.NDEF_TAG_DISCOVERED"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
-        </receiver>
+        </activity>
 
     </application>
-    <uses-permission android:name="com.trustedlogic.trustednfc.permission.NFC_NOTIFY"></uses-permission>
-    <uses-permission android:name="com.trustedlogic.trustednfc.permission.NFC_RAW"></uses-permission>
 </manifest>
diff --git a/apps/Tag/res/values/styles.xml b/apps/Tag/res/layout/tag_divider.xml
similarity index 76%
rename from apps/Tag/res/values/styles.xml
rename to apps/Tag/res/layout/tag_divider.xml
index 40e29fb..b6b1b7c 100644
--- a/apps/Tag/res/values/styles.xml
+++ b/apps/Tag/res/layout/tag_divider.xml
@@ -14,10 +14,9 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<resources>
 
-  <style name="Tags.TagBrowserTheme" parent="@android:style/Theme.NoTitleBar">
-  </style>
-
-
-</resources>
+<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"
+/>
\ No newline at end of file
diff --git a/apps/Tag/res/values/styles.xml b/apps/Tag/res/layout/tag_text.xml
similarity index 64%
copy from apps/Tag/res/values/styles.xml
copy to apps/Tag/res/layout/tag_text.xml
index 40e29fb..9038650 100644
--- a/apps/Tag/res/values/styles.xml
+++ b/apps/Tag/res/layout/tag_text.xml
@@ -14,10 +14,13 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<resources>
 
-  <style name="Tags.TagBrowserTheme" parent="@android:style/Theme.NoTitleBar">
-  </style>
-
-
-</resources>
+<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"
+/>
\ No newline at end of file
diff --git a/apps/Tag/res/values/styles.xml b/apps/Tag/res/layout/tag_viewer_list.xml
similarity index 68%
copy from apps/Tag/res/values/styles.xml
copy to apps/Tag/res/layout/tag_viewer_list.xml
index 40e29fb..3d322d5 100644
--- a/apps/Tag/res/values/styles.xml
+++ b/apps/Tag/res/layout/tag_viewer_list.xml
@@ -1,23 +1,21 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2010 The Android Open Source Project
+<!-- 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="Tags.TagBrowserTheme" parent="@android:style/Theme.NoTitleBar">
-  </style>
-
-
-</resources>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/list"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+/>
diff --git a/apps/Tag/res/values/strings.xml b/apps/Tag/res/values/strings.xml
index 3d04327..9b73678 100644
--- a/apps/Tag/res/values/strings.xml
+++ b/apps/Tag/res/values/strings.xml
@@ -20,4 +20,10 @@
     <string name="help_and_info">help and info</string>
     <string name="saved">Saved</string>
 
+    <!-- The title of the tab that displays all recently scanned NFC tags -->
+    <string name="tab_recent">Recent</string>
+
+    <!-- The title of the tab that displays all saved NFC tags -->
+    <string name="tab_saved">Saved</string>
+
 </resources>
diff --git a/apps/Tag/src/com/android/apps/tag/NdefUtil.java b/apps/Tag/src/com/android/apps/tag/NdefUtil.java
index 87efb9e..40f5bcf 100644
--- a/apps/Tag/src/com/android/apps/tag/NdefUtil.java
+++ b/apps/Tag/src/com/android/apps/tag/NdefUtil.java
@@ -16,17 +16,17 @@
 
 package com.android.apps.tag;
 
-import android.util.Log;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.Iterables;
 import com.google.common.primitives.Bytes;
+
+import android.net.Uri;
 import android.nfc.NdefMessage;
 import android.nfc.NdefRecord;
 
 import java.io.UnsupportedEncodingException;
-import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.charset.Charsets;
 import java.util.ArrayList;
@@ -47,8 +47,7 @@
      * This is a mapping of "URI Identifier Codes" to URI string prefixes,
      * per section 3.2.2 of the NFC Forum URI Record Type Definition document.
      */
-    private static final
-    BiMap<Byte, String> URI_PREFIX_MAP = ImmutableBiMap.<Byte, String>builder()
+    private static final BiMap<Byte, String> URI_PREFIX_MAP = ImmutableBiMap.<Byte, String>builder()
             .put((byte) 0x00, "")
             .put((byte) 0x01, "http://www.")
             .put((byte) 0x02, "https://www.")
@@ -88,9 +87,9 @@
             .build();
 
     /**
-     * Create a new {@link NdefRecord} containing the supplied {@link URI}.
+     * Create a new {@link NdefRecord} containing the supplied {@link Uri}.
      */
-    public static NdefRecord toUriRecord(URI uri) {
+    public static NdefRecord toUriRecord(Uri uri) {
         byte[] uriBytes = uri.toString().getBytes(Charsets.UTF_8);
 
         /*
@@ -110,18 +109,16 @@
     }
 
     /**
-     * Convert {@link NdefRecord} into a {@link URI}.
+     * Convert {@link NdefRecord} into a {@link Uri}.
      *
      * TODO: This class does not handle NdefRecords where the TNF
      * (Type Name Format) of the class is {@link NdefRecord#TNF_ABSOLUTE_URI}.
      * This should be fixed.
      *
-     * @throws URISyntaxException if the {@code NdefRecord} contains an
-     *     invalid URI.
      * @throws IllegalArgumentException if the NdefRecord is not a
      *     record containing a URI.
      */
-    public static URI toURI(NdefRecord record) throws URISyntaxException {
+    public static Uri toUri(NdefRecord record) {
         Preconditions.checkArgument(record.getTnf() == NdefRecord.TNF_WELL_KNOWN);
         Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_URI));
 
@@ -140,17 +137,15 @@
                 prefix.getBytes(Charsets.UTF_8),
                 Arrays.copyOfRange(payload, 1, payload.length));
 
-        return new URI(new String(fullUri, Charsets.UTF_8));
+        return Uri.parse(new String(fullUri, Charsets.UTF_8));
     }
 
-    public static boolean isURI(NdefRecord record) {
+    public static boolean isUri(NdefRecord record) {
         try {
-            toURI(record);
+            toUri(record);
             return true;
         } catch (IllegalArgumentException e) {
             return false;
-        } catch (URISyntaxException e) {
-            return false;
         }
     }
 
@@ -207,33 +202,29 @@
         return Iterables.filter(getObjects(message), String.class);
     }
 
-    public static Iterable<URI> getURIs(NdefMessage message) {
-        return Iterables.filter(getObjects(message), URI.class);
+    public static Iterable<Uri> getUris(NdefMessage message) {
+        return Iterables.filter(getObjects(message), Uri.class);
     }
 
     /**
      * Parse the provided {@code NdefMessage}, extracting all known
      * objects from the message.  Typically this list will consist of
-     * {@link String}s corresponding to NDEF text records, or {@link URI}s
+     * {@link String}s corresponding to NDEF text records, or {@link Uri}s
      * corresponding to NDEF URI records.
      * <p>
      * TODO: Is this API too generic?  Should we keep it?
      */
-    private static Iterable<Object> getObjects(NdefMessage message) {
-        try {
-            List<Object> retval = new ArrayList<Object>();
-            for (NdefRecord record : message.getRecords()) {
-                if (isURI(record)) {
-                    retval.add(toURI(record));
-                } else if (isText(record)) {
-                    retval.add(toText(record));
-                } else if (SmartPoster.isPoster(record)) {
-                    retval.add(SmartPoster.from(record));
-                }
+    public static Iterable<Object> getObjects(NdefMessage message) {
+        List<Object> retval = new ArrayList<Object>();
+        for (NdefRecord record : message.getRecords()) {
+            if (isUri(record)) {
+                retval.add(toUri(record));
+            } else if (isText(record)) {
+                retval.add(toText(record));
+            } else if (SmartPoster.isPoster(record)) {
+                retval.add(SmartPoster.from(record));
             }
-            return retval;
-        } catch (URISyntaxException e) {
-            throw new IllegalArgumentException(e);
         }
+        return retval;
     }
 }
diff --git a/apps/Tag/src/com/android/apps/tag/SaveTag.java b/apps/Tag/src/com/android/apps/tag/SaveTag.java
deleted file mode 100644
index 6a5f66f..0000000
--- a/apps/Tag/src/com/android/apps/tag/SaveTag.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * 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.content.DialogInterface;
-import android.nfc.NdefMessage;
-import android.nfc.NdefTag;
-import android.nfc.NfcAdapter;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.WindowManager;
-import android.widget.Toast;
-
-/**
- * An {@code Activity} which handles a broadcast of a new tag that the device just discovered.
- */
-public class SaveTag extends Activity implements DialogInterface.OnClickListener {
-    private static final String TAG = "SaveTag";
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-
-        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
-        );
-
-        showDialog(1);
-        NdefTag tag = getIntent().getParcelableExtra(NfcAdapter.EXTRA_TAG);
-        NdefMessage[] msgs = tag.getNdefMessages();
-
-        if (msgs.length == 0) {
-            Log.d(TAG, "No NDEF messages");
-            return;
-        }
-        if (msgs.length > 1) {
-            Log.d(TAG, "Multiple NDEF messages, only saving first");
-        }
-        String s = toHexString(msgs[0].toByteArray());
-        Log.d("SaveTag", s);
-        Toast.makeText(this.getBaseContext(), "SaveTag: " + s, Toast.LENGTH_SHORT).show();
-    }
-
-    @Override
-    protected Dialog onCreateDialog(int id, Bundle args) {
-        return new AlertDialog.Builder(this)
-                .setTitle("Welcome! T2000 Festival")
-                .setPositiveButton("Save", this)
-                .setNegativeButton("Cancel", this)
-                .create();
-    }
-
-    @Override
-    public void onClick(DialogInterface dialog, int which) {
-        finish();
-    }
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-        dismissDialog(1);
-    }
-
-    private static final char[] hexDigits = "0123456789abcdef".toCharArray();
-
-    private static String toHexString(byte[] bytes) {
-        StringBuilder sb = new StringBuilder(3 * bytes.length);
-        for (byte b : bytes) {
-            sb.append("(byte) 0x")
-                .append(hexDigits[(b >> 4) & 0xf])
-                .append(hexDigits[b & 0xf]).append(", ");
-        }
-        return sb.toString();
-    }
-}
diff --git a/apps/Tag/src/com/android/apps/tag/SmartPoster.java b/apps/Tag/src/com/android/apps/tag/SmartPoster.java
index 1e10723..dd6f518 100644
--- a/apps/Tag/src/com/android/apps/tag/SmartPoster.java
+++ b/apps/Tag/src/com/android/apps/tag/SmartPoster.java
@@ -18,13 +18,15 @@
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Iterables;
+
+import android.net.Uri;
+import android.nfc.FormatException;
 import android.nfc.NdefMessage;
 import android.nfc.NdefRecord;
-import android.nfc.FormatException;
+
+import java.util.Arrays;
 
 import javax.annotation.Nullable;
-import java.net.URI;
-import java.util.Arrays;
 
 /**
  * A representation of an NFC Forum "Smart Poster".
@@ -39,7 +41,7 @@
      * This record is optional."
 
      */
-    private final String titleRecord;
+    private final String mTitleRecord;
 
     /**
      * NFC Forum Smart Poster Record Type Definition section 3.2.1.
@@ -48,22 +50,22 @@
      * records are just metadata about this record. There MUST be one URI
      * record and there MUST NOT be more than one."
      */
-    private final URI uriRecord;
+    private final Uri mUriRecord;
 
-    private SmartPoster(URI uri, @Nullable String title) {
-        uriRecord = Preconditions.checkNotNull(uri);
-        titleRecord = title;
+    private SmartPoster(Uri uri, @Nullable String title) {
+        mUriRecord = Preconditions.checkNotNull(uri);
+        mTitleRecord = title;
     }
 
-    public URI getURI() {
-        return uriRecord;
+    public Uri getUri() {
+        return mUriRecord;
     }
 
     /**
-     * Returns the title of the smartposter.  This may be {@code null}.
+     * Returns the title of the smart poster.  This may be {@code null}.
      */
     public String getTitle() {
-        return titleRecord;
+        return mTitleRecord;
     }
 
     public static SmartPoster from(NdefRecord record) {
@@ -71,7 +73,7 @@
         Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_SMART_POSTER));
         try {
             NdefMessage subRecords = new NdefMessage(record.getPayload());
-            URI uri = Iterables.getOnlyElement(NdefUtil.getURIs(subRecords));
+            Uri uri = Iterables.getOnlyElement(NdefUtil.getUris(subRecords));
             Iterable<String> textFields = NdefUtil.getTextFields(subRecords);
             String title = null;
             if (!Iterables.isEmpty(textFields)) {
diff --git a/apps/Tag/src/com/android/apps/tag/TagAdapter.java b/apps/Tag/src/com/android/apps/tag/TagAdapter.java
new file mode 100644
index 0000000..f69c3ea
--- /dev/null
+++ b/apps/Tag/src/com/android/apps/tag/TagAdapter.java
@@ -0,0 +1,86 @@
+/*
+ * 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.net.Uri;
+import android.nfc.FormatException;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.text.format.DateUtils;
+import android.util.Log;
+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);
+
+        NdefMessage msg = null;
+        try {
+            msg = new NdefMessage(cursor.getBlob(cursor.getColumnIndex(NdefMessagesTable.BYTES)));
+        } catch (FormatException e) {
+            Log.e("foo", "poorly formatted message", e);
+        }
+
+        if (msg == null) {
+            mainLine.setText("Invalid tag");
+        } else {
+            try {
+                SmartPoster poster = SmartPoster.from(msg.getRecords()[0]);
+                mainLine.setText(poster.getTitle());
+            } catch (IllegalArgumentException e) {
+                // Not a smart poster
+                NdefRecord record = msg.getRecords()[0];
+                Uri uri = null;
+                try {
+                    uri = NdefUtil.toUri(record);
+                    mainLine.setText(uri.toString());
+                } catch (IllegalArgumentException e2) {
+                    mainLine.setText("Not a smart poster or URL");
+                }
+            }
+        }
+        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);
+    }
+}
diff --git a/apps/Tag/src/com/android/apps/tag/TagBroadcastReceiver.java b/apps/Tag/src/com/android/apps/tag/TagBroadcastReceiver.java
deleted file mode 100644
index b5ef1e7..0000000
--- a/apps/Tag/src/com/android/apps/tag/TagBroadcastReceiver.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.nfc.NdefTag;
-
-import android.nfc.NdefMessage;
-import android.nfc.NfcAdapter;
-
-/**
- * When we receive a new NDEF tag, start the activity to
- * process the tag.
- */
-public class TagBroadcastReceiver extends BroadcastReceiver {
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        if (intent.getAction().equals(NfcAdapter.ACTION_NDEF_TAG_DISCOVERED)) {
-            NdefTag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
-            Intent i = new Intent(context, SaveTag.class)
-                    .putExtra(NfcAdapter.EXTRA_TAG, tag)
-                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            context.startActivity(i);
-        }
-    }
-}
diff --git a/apps/Tag/src/com/android/apps/tag/TagBrowserActivity.java b/apps/Tag/src/com/android/apps/tag/TagBrowserActivity.java
index a4cbb86..38d3bfa 100644
--- a/apps/Tag/src/com/android/apps/tag/TagBrowserActivity.java
+++ b/apps/Tag/src/com/android/apps/tag/TagBrowserActivity.java
@@ -16,6 +16,7 @@
 
 package com.android.apps.tag;
 
+import android.app.Activity;
 import android.app.TabActivity;
 import android.content.Intent;
 import android.content.res.Resources;
@@ -23,34 +24,27 @@
 import android.widget.TabHost;
 
 /**
- * A browsing {@code Activity} that displays the saved tags in categories under tabs.
+ * 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);
 
-        // While we're doing development, delete the database every time we start.
-        getBaseContext().getDatabasePath("Tags.db").delete();
-
         setContentView(R.layout.main);
 
         Resources res = getResources();
         TabHost tabHost = getTabHost();
-        Intent i = new Intent().setClass(this, TagList.class);
 
-        Intent iSavedList = new Intent().setClass(this, TagList.class)
-                .putExtra(TagList.SHOW_SAVED_ONLY, true);
-        Intent iRecentList = new Intent().setClass(this, TagList.class);
-
-        TabHost.TabSpec spec1 = tabHost.newTabSpec("1")
-                .setIndicator("Saved", res.getDrawable(R.drawable.ic_menu_tag))
-                .setContent(iSavedList);
+        TabHost.TabSpec spec1 = tabHost.newTabSpec("saved")
+                .setIndicator(getText(R.string.tab_saved), res.getDrawable(R.drawable.ic_menu_tag))
+                .setContent(new Intent().setClass(this, TagList.class)
+                        .putExtra(TagList.EXTRA_SHOW_SAVED_ONLY, true));
         tabHost.addTab(spec1);
 
-        TabHost.TabSpec spec2 = tabHost.newTabSpec("2")
-                .setIndicator("Recent", res.getDrawable(R.drawable.ic_menu_desk_clock))
-                .setContent(iRecentList);
+        TabHost.TabSpec spec2 = tabHost.newTabSpec("recent")
+                .setIndicator(getText(R.string.tab_recent), res.getDrawable(R.drawable.ic_menu_desk_clock))
+                .setContent(new Intent().setClass(this, TagList.class));
         tabHost.addTab(spec2);
     }
 }
diff --git a/apps/Tag/src/com/android/apps/tag/TagCursorAdapter.java b/apps/Tag/src/com/android/apps/tag/TagCursorAdapter.java
deleted file mode 100644
index a658268..0000000
--- a/apps/Tag/src/com/android/apps/tag/TagCursorAdapter.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2010 Google Inc. All Rights Reserved.
-
-package com.android.apps.tag;
-
-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 TagCursorAdapter extends CursorAdapter {
-
-    private final LayoutInflater mInflater;
-
-    public TagCursorAdapter(Context context, Cursor c) {
-        super(context, c);
-
-        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);
-
-        // TODO(benkomalo): either write a cursor abstraction, or use constants for column indices.
-        mainLine.setText(cursor.getString(cursor.getColumnIndex("bytes")));
-        dateLine.setText(DateUtils.getRelativeTimeSpanString(
-                context, cursor.getLong(cursor.getColumnIndex("date"))));
-    }
-
-    @Override
-    public View newView(Context context, Cursor cursor, ViewGroup parent) {
-        return mInflater.inflate(R.layout.tag_list_item, null);
-    }
-}
diff --git a/apps/Tag/src/com/android/apps/tag/TagDBHelper.java b/apps/Tag/src/com/android/apps/tag/TagDBHelper.java
index 06fa9e0..654cb47 100644
--- a/apps/Tag/src/com/android/apps/tag/TagDBHelper.java
+++ b/apps/Tag/src/com/android/apps/tag/TagDBHelper.java
@@ -22,34 +22,32 @@
 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.net.URI;
-import java.util.Date;
-
 /**
  * Database utilities for the saved tags.
  */
 public class TagDBHelper extends SQLiteOpenHelper {
 
-    private static final int DATABASE_VERSION = 1;
+    private static final String DATABASE_NAME = "tags.db";
+    private static final int DATABASE_VERSION = 3;
 
-    private static final String NDEF_MSG = "create table NdefMessage ("
-            + "_id INTEGER NOT NULL, "
-            + "bytes BLOB NOT NULL, "
-            + "date INTEGER NOT NULL, "
-            + "saved TEXT NOT NULL default 0,"  // boolean
-            + "PRIMARY KEY(_id)"
-            + ")";
+    public interface NdefMessagesTable {
+        public static final String TABLE_NAME = "nedf_msg";
 
-    private static final String INSERT =
-            "INSERT INTO NdefMessage (bytes, date, saved) values (?, ?, ?)";
-
+        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 SAVED = "saved";
+    }
+    
     /**
      * 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
+     * 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[] {
@@ -115,53 +113,85 @@
             // end smart poster payload
     };
 
-    public TagDBHelper(Context context) {
-        this(context, "Tags.db");
+    private static TagDBHelper sInstance;
+
+    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);
     }
 
     @VisibleForTesting
-    public TagDBHelper(Context context, String dbFile) {
+    TagDBHelper(Context context, String dbFile) {
         super(context, dbFile, null, DATABASE_VERSION);
     }
 
     @Override
     public void onCreate(SQLiteDatabase db) {
-        db.execSQL(NDEF_MSG);
+        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.SAVED + " INTEGER NOT NULL DEFAULT 0" +  // boolean
+                ");");
 
+        db.execSQL("CREATE INDEX msgIndex ON " + NdefMessagesTable.TABLE_NAME + " (" +
+                NdefMessagesTable.DATE + " DESC, " +
+                NdefMessagesTable.SAVED + " 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.create("http://www.google.com"))
+                NdefUtil.toUriRecord(Uri.parse("http://www.google.com"))
         });
 
         // A fake message containing 2 URLs
         NdefMessage msg2 = new NdefMessage(new NdefRecord[] {
-                NdefUtil.toUriRecord(URI.create("http://www.youtube.com")),
-                NdefUtil.toUriRecord(URI.create("http://www.android.com"))
+                NdefUtil.toUriRecord(Uri.parse("http://www.youtube.com")),
+                NdefUtil.toUriRecord(Uri.parse("http://www.android.com"))
         });
 
-        insert(db, msg1, false);
-        insert(db, msg2, true);
+        insertNdefMessage(db, msg1, false);
+        insertNdefMessage(db, msg2, true);
 
         try {
             // A real message obtained from an NFC Forum Type 4 tag.
             NdefMessage msg3 = new NdefMessage(REAL_NFC_MSG);
-            insert(db, msg3, false);
+            insertNdefMessage(db, msg3, false);
         } catch (FormatException e) {
             throw new RuntimeException(e);
         }
     }
 
-    private void insert(SQLiteDatabase db, NdefMessage msg, boolean isSaved) {
-        SQLiteStatement stmt = db.compileStatement(INSERT);
-        stmt.bindString(1, new String(msg.toByteArray())); // TODO: This should be a blob
-        stmt.bindLong(2, System.currentTimeMillis());
-        String isSavedStr = isSaved ? "1" : "0";
-        stmt.bindString(3, isSavedStr);
-        stmt.executeInsert();
-        stmt.close();
-    }
-
-    @Override
-    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+    public void insertNdefMessage(SQLiteDatabase db, NdefMessage msg, boolean isSaved) {
+        SQLiteStatement stmt = null;
+        try {
+            stmt = db.compileStatement("INSERT INTO " + NdefMessagesTable.TABLE_NAME +
+                    "(" + NdefMessagesTable.BYTES + ", " + NdefMessagesTable.DATE + ", " +
+                    NdefMessagesTable.SAVED + ") values (?, ?, ?)");
+            stmt.bindBlob(1, msg.toByteArray());
+            stmt.bindLong(2, System.currentTimeMillis());
+            stmt.bindLong(3, isSaved ? 1 : 0);
+            stmt.executeInsert();
+        } finally {
+            if (stmt != null) stmt.close();
+        }
     }
 }
diff --git a/apps/Tag/src/com/android/apps/tag/TagList.java b/apps/Tag/src/com/android/apps/tag/TagList.java
index 369ef65..45f6f65 100644
--- a/apps/Tag/src/com/android/apps/tag/TagList.java
+++ b/apps/Tag/src/com/android/apps/tag/TagList.java
@@ -16,42 +16,47 @@
 
 package com.android.apps.tag;
 
+import com.android.apps.tag.TagDBHelper.NdefMessagesTable;
+
+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 android.widget.SimpleCursorAdapter;
 
 /**
- * An {@code Activity} that displays a flat list of tags that can be "opened".
+ * An {@link Activity} that displays a flat list of tags that can be "opened".
  */
 public class TagList extends ListActivity implements DialogInterface.OnClickListener {
-    private SQLiteDatabase db;
-    private Cursor cursor;
-    static final String SHOW_SAVED_ONLY = "show_saved_only";
+    static final String TAG = "TagList";
+
+    static final String EXTRA_SHOW_SAVED_ONLY = "show_saved_only";
+
+    SQLiteDatabase mDatabase;
+    TagAdapter mAdapter;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        boolean showSavedOnly = getIntent().getBooleanExtra(SHOW_SAVED_ONLY, false);
-        db = new TagDBHelper(getBaseContext()).getReadableDatabase();
-        String selection = showSavedOnly ? "saved=1" : null;
+        boolean showSavedOnly = getIntent().getBooleanExtra(EXTRA_SHOW_SAVED_ONLY, false);
+        mDatabase = TagDBHelper.getInstance(this).getReadableDatabase();
+        String selection = showSavedOnly ? NdefMessagesTable.SAVED + "=1" : null;
 
-        // TODO: Use an AsyncQueryHandler so that DB queries are not done on UI thread.
-        cursor = db.query(
-                "NdefMessage",
-                new String[] { "_id", "bytes", "date" },
-                selection,
-                null, null, null, null);
-
-        setListAdapter(new TagCursorAdapter(this, cursor));
+        new TagLoaderTask().execute(selection);
+        mAdapter = new TagAdapter(this);
+        setListAdapter(mAdapter);
         registerForContextMenu(getListView());
     }
 
@@ -75,22 +80,53 @@
 
     @Override
     protected void onDestroy() {
-        if (cursor != null) {
-            cursor.close();
-        }
-        if (db != null) {
-            db.close();
+        if (mAdapter != null) {
+            mAdapter.changeCursor(null);
         }
         super.onDestroy();
     }
 
     @Override
     protected void onListItemClick(ListView l, View v, int position, long id) {
-        showDialog(1);
-        super.onListItemClick(l, v, position, 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, null);
+            cursor.getCount();
+            return cursor;
+        }
+
+        @Override
+        protected void onPostExecute(Cursor cursor) {
+            mAdapter.changeCursor(cursor);
+        }
+    }
 }
diff --git a/apps/Tag/src/com/android/apps/tag/TagViewer.java b/apps/Tag/src/com/android/apps/tag/TagViewer.java
new file mode 100644
index 0000000..21f64e1
--- /dev/null
+++ b/apps/Tag/src/com/android/apps/tag/TagViewer.java
@@ -0,0 +1,137 @@
+/*
+ * 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.content.Intent;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Color;
+import android.net.Uri;
+import android.nfc.NdefMessage;
+import android.nfc.NdefTag;
+import android.nfc.NfcAdapter;
+import android.os.AsyncTask;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * An {@link Activity} which handles a broadcast of a new tag that the device just discovered.
+ */
+public class TagViewer extends Activity {
+    static final String TAG = "SaveTag";    
+    static final String EXTRA_TAG_DB_ID = "db_id";
+    static final String EXTRA_MESSAGE = "msg";
+
+    long mTagDatabaseId;
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+
+        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
+        );
+
+        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 };
+            }
+        } else {
+            msgs = tag.getNdefMessages();
+            // TODO use a service to avoid the process getting reaped during saving 
+            new SaveTagTask().execute(msgs);
+        }
+
+        if (msgs == null || msgs.length == 0) {
+            Log.e(TAG, "No NDEF messages");
+            finish();
+            return;
+        }
+
+        
+        LayoutInflater inflater = LayoutInflater.from(
+                new ContextThemeWrapper(this, android.R.style.Theme_Light));
+        LinearLayout list = (LinearLayout) inflater.inflate(R.layout.tag_viewer_list, null, false);
+        // TODO figure out why the background isn't white, the CTW should force that...
+        list.setBackgroundColor(Color.WHITE);
+        setContentView(list);
+        buildTagViews(list, inflater, msgs);
+    }
+
+    private void buildTagViews(LinearLayout list, LayoutInflater inflater, NdefMessage[] msgs) {
+        // The body of the dialog should use the light theme
+
+        // Build the views from the logical records in the messages
+        boolean first = true;
+        for (NdefMessage msg : msgs) {
+            Iterable<Object> objects = NdefUtil.getObjects(msg);
+            for (Object object : objects) {
+                if (!first) {
+                    list.addView(inflater.inflate(R.layout.tag_divider, list, false));
+                    first = false;
+                }
+
+                if (object instanceof String) {
+                    TextView text = (TextView) inflater.inflate(R.layout.tag_text, list, false);
+                    text.setText((CharSequence) object);
+                    list.addView(text);
+                } else if (object instanceof Uri) {
+                    TextView text = (TextView) inflater.inflate(R.layout.tag_text, list, false);
+                    text.setText(object.toString());
+                    list.addView(text);
+                } else if (object instanceof SmartPoster) {
+                    TextView text = (TextView) inflater.inflate(R.layout.tag_text, list, false);
+                    SmartPoster poster = (SmartPoster) object;
+                    text.setText(poster.getTitle());
+                    list.addView(text);
+                }
+            }
+        }
+    }
+    
+    final class SaveTagTask extends AsyncTask<NdefMessage, Void, Void> {
+        @Override
+        public Void doInBackground(NdefMessage... msgs) {
+            TagDBHelper helper = TagDBHelper.getInstance(TagViewer.this);
+            SQLiteDatabase db = helper.getWritableDatabase();
+            db.beginTransaction();
+            try {
+                for (NdefMessage msg : msgs) {
+                    helper.insertNdefMessage(db, msg, false);
+                }
+                db.setTransactionSuccessful();
+            } finally {
+                db.endTransaction();
+            }
+            return null;
+        }
+    }
+}
diff --git a/apps/Tag/tests/src/com/android/apps/tag/SmartPosterTest.java b/apps/Tag/tests/src/com/android/apps/tag/SmartPosterTest.java
index b7eb869..e55e07d 100644
--- a/apps/Tag/tests/src/com/android/apps/tag/SmartPosterTest.java
+++ b/apps/Tag/tests/src/com/android/apps/tag/SmartPosterTest.java
@@ -28,6 +28,6 @@
 
         SmartPoster poster = SmartPoster.from(msg.getRecords()[0]);
         assertEquals("NFC Forum Type 4 Tag", poster.getTitle());
-        assertEquals("http://www.nxp.com/nfc", poster.getURI().toString());
+        assertEquals("http://www.nxp.com/nfc", poster.getUri().toString());
     }
 }