am 25ffbb26: Import revised translations. DO NOT MERGE
Eric Fischer [Fri, 28 Jan 2011 23:14:07 +0000 (15:14 -0800)]
* commit '25ffbb26da612071e4dc4a3950b0c8abb2642c08':
  Import revised translations.  DO NOT MERGE

32 files changed:
Android.mk
AndroidManifest.xml
res/drawable-hdpi/active_tag_icon.png [new file with mode: 0644]
res/drawable-mdpi/active_tag_icon.png [new file with mode: 0644]
res/layout/edit_tag_activity.xml
res/layout/my_tag_activity.xml
res/layout/tag_divider.xml
res/layout/tag_edit_image.xml
res/layout/tag_edit_url.xml
res/layout/tag_edit_vcard.xml
res/layout/tag_list_item.xml
res/layout/write_tag.xml [new file with mode: 0644]
res/menu/my_tag_list_context_menu.xml [new file with mode: 0644]
res/values-es/strings.xml
res/values/strings.xml
res/xml/filter_nfc.xml [new file with mode: 0644]
src/com/android/apps/tag/EditTagActivity.java
src/com/android/apps/tag/MyTagActivity.java [deleted file]
src/com/android/apps/tag/MyTagList.java
src/com/android/apps/tag/TagBrowserActivity.java
src/com/android/apps/tag/TagContentSelector.java
src/com/android/apps/tag/TagList.java
src/com/android/apps/tag/TagService.java
src/com/android/apps/tag/TagViewer.java
src/com/android/apps/tag/WriteTagActivity.java [new file with mode: 0644]
src/com/android/apps/tag/provider/TagDBHelper.java
src/com/android/apps/tag/provider/TagProvider.java
src/com/android/apps/tag/record/ImageRecord.java
src/com/android/apps/tag/record/RecordEditInfo.java
src/com/android/apps/tag/record/UriRecord.java
src/com/android/apps/tag/record/VCardRecord.java
tests/Android.mk

index 9ec93b1..7fa60a0 100644 (file)
@@ -6,13 +6,13 @@ 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_SRC_FILES := $(call all-java-files-under, src)
 
 LOCAL_PACKAGE_NAME := Tag
 
 #LOCAL_SDK_VERSION := current
 
-#include $(BUILD_PACKAGE)
+include $(BUILD_PACKAGE)
 
 # Use the following include to make our test apk.
-#include $(call all-makefiles-under,$(LOCAL_PATH))
+include $(call all-makefiles-under,$(LOCAL_PATH))
index 90c0896..ecc63e3 100644 (file)
@@ -16,8 +16,8 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.apps.tag"
-    android:versionCode="100"
-    android:versionName="1.0"
+    android:versionCode="101"
+    android:versionName="1.1"
 >
 
     <uses-permission android:name="android.permission.CALL_PHONE" />
@@ -34,6 +34,7 @@
     >
         <activity android:name="TagBrowserActivity"
             android:theme="@android:style/Theme.NoTitleBar"
+            android:screenOrientation="portrait"
         >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
 
         <activity android:name="TagList" />
 
-        <activity android:name="MyTagList" />
+        <!-- TODO: lift the portrait restriction by re-working the list in the
+             layout. -->
+        <activity android:name="MyTagList"
+            android:screenOrientation="portrait" />
 
         <activity android:name="TagViewer"
             android:theme="@android:style/Theme.NoTitleBar"
             <!-- Offer to display anything with NDEF data -->
             <intent-filter>
                 <action android:name="android.nfc.action.TECH_DISCOVERED"/>
-                <data android:scheme="vnd.android.nfc" android:host="tag" android:pathPattern=".*\/6\/.*" />
-                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
 
+            <meta-data android:name="android.nfc.action.TECH_DISCOVERED"
+                android:resource="@xml/filter_nfc"
+            />
+
             <intent-filter>
                 <action android:name="android.intent.action.VIEW"/>
                 <data android:mimeType="vnd.android.cursor.item/ndef_msg"/>
@@ -69,7 +75,8 @@
             </intent-filter>
         </activity>
 
-        <activity android:name="MyTagActivity" android:label="@string/tab_my_tag">
+        <!-- Make the activity show up as "My tag" when resolving intent -->
+        <activity android:name="EditTagActivity" android:label="@string/tab_my_tag">
             <intent-filter>
                 <action android:name="android.intent.action.SEND"/>
                 <category android:name="android.intent.category.DEFAULT"/>
             android:screenOrientation="portrait"
         />
 
+        <activity android:name="WriteTagActivity"
+            android:launchMode="singleTop"
+            android:screenOrientation="portrait"
+        />
+
         <service android:name="TagService" />
 
         <provider android:name=".provider.TagProvider"
diff --git a/res/drawable-hdpi/active_tag_icon.png b/res/drawable-hdpi/active_tag_icon.png
new file mode 100644 (file)
index 0000000..cdc05a8
Binary files /dev/null and b/res/drawable-hdpi/active_tag_icon.png differ
diff --git a/res/drawable-mdpi/active_tag_icon.png b/res/drawable-mdpi/active_tag_icon.png
new file mode 100644 (file)
index 0000000..30ead23
Binary files /dev/null and b/res/drawable-mdpi/active_tag_icon.png differ
index 151cd48..703980d 100644 (file)
@@ -23,80 +23,33 @@ limitations under the License.
 >
 
     <ScrollView
-        android:layout_weight="1"
+        android:id="@+id/content_parent"
+        android:layout_weight="1.0"
         android:layout_width="match_parent"
-        android:layout_height="0dip">
-
-        <LinearLayout
-            android:orientation="vertical"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content">
-
-            <!-- Tag text -->
-            <LinearLayout
-                android:padding="8dip"
-                android:orientation="vertical"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content" >
-
-                <TextView
-                    android:text="@string/tag_text"
-                    style="@style/record_title"
-                    />
-
-                <EditText
-                    android:id="@+id/input_tag_text"
-                    android:inputType="textMultiLine"
-                    android:lines="3"
-                    android:gravity="top"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content" />
-
-            </LinearLayout>
-
-            <LinearLayout
-                android:id="@+id/content_parent"
-                android:orientation="vertical"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content" />
-
-            <include layout="@layout/tag_divider" />
-
-            <!-- Control to add content. -->
-            <TextView
-                android:id="@+id/add_content_target"
-                android:text="@string/add_content"
-                android:textAppearance="?android:attr/textAppearanceMedium"
-                android:paddingLeft="8dip"
-                android:paddingRight="8dip"
-                android:paddingTop="16dip"
-                android:paddingBottom="16dip"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content" />
-
-            <include layout="@layout/tag_divider" />
-
-        </LinearLayout>
-    </ScrollView>
+        android:layout_height="0dip" />
 
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_gravity="bottom"
+        android:layout_alignParentBottom="true"
+
+        android:orientation="horizontal"
+        style="@android:style/ButtonBar"
     >
         <Button
             android:id="@+id/save"
             android:text="@string/button_save"
-            android:layout_weight="0.5"
+            android:layout_weight="1"
             android:layout_width="match_parent"
             android:layout_height="wrap_content" />
 
         <Button
             android:id="@+id/cancel"
             android:text="@string/button_cancel"
-            android:layout_weight="0.5"
+            android:layout_weight="1"
             android:layout_width="match_parent"
             android:layout_height="wrap_content" />
 
     </LinearLayout>
+
 </LinearLayout>
index 8c91f45..d7e2800 100644 (file)
@@ -92,8 +92,6 @@ limitations under the License.
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content" />
 
-            <!-- TODO: green active image -->
-
         </RelativeLayout>
 
         <TextView
@@ -121,21 +119,33 @@ limitations under the License.
         android:layout_height="0dip"
     />
 
-    <include layout="@layout/tag_divider" />
-
-    <!-- TODO: add this to end of list, instead. -->
     <!-- Control to add new tag. -->
-    <TextView
-        android:id="@+id/add_tag"
-        android:text="@string/add_tag"
-        android:textAppearance="?android:attr/textAppearanceMedium"
-        android:paddingLeft="8dip"
-        android:paddingRight="8dip"
-        android:paddingTop="16dip"
-        android:paddingBottom="16dip"
+    <LinearLayout
         android:layout_width="match_parent"
-        android:layout_height="wrap_content" />
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        style="@android:style/ButtonBar"
+    >
 
-    <include layout="@layout/tag_divider" />
+        <View
+            android:layout_height="0dip"
+            android:layout_width="0dip"
+            android:layout_weight="1"
+        />
+
+        <Button
+            android:id="@+id/add_tag"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="2"
+            android:text="@string/add_tag"
+        />
+
+        <View
+            android:layout_height="0dip"
+            android:layout_width="0dip"
+            android:layout_weight="1"
+        />
+    </LinearLayout>
 </LinearLayout>
 
index 58d8911..049c588 100644 (file)
@@ -17,6 +17,6 @@
 
 <View xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
+    android:layout_height="2dip"
     android:background="?android:attr/listDivider"
 />
index bdd07cf..b924fc0 100644 (file)
     android:layout_width="match_parent"
     android:layout_height="wrap_content">
 
-    <ImageView
-        android:src="@drawable/ic_btn_round_minus"
-        android:id="@+id/delete"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_alignParentTop="true"
-        android:layout_alignParentRight="true"
-    />
-
     <TextView
         android:id="@+id/title"
         android:text="@string/photo"
         style="@style/record_title"
         android:layout_alignParentTop="true"
-        android:layout_toLeftOf="@+id/delete"
     />
 
     <ImageView
index 86758e2..568735b 100644 (file)
     android:layout_width="match_parent"
     android:layout_height="wrap_content">
 
-    <ImageView
-        android:src="@drawable/ic_btn_round_minus"
-        android:id="@+id/delete"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_alignParentTop="true"
-        android:layout_alignParentRight="true"
-    />
-
     <TextView
         android:id="@+id/title"
         android:text="@string/url"
         style="@style/record_title"
         android:layout_alignParentTop="true"
-        android:layout_toLeftOf="@+id/delete"
         />
 
     <EditText
index d2b6b9a..90e8d86 100644 (file)
     android:layout_width="match_parent"
     android:layout_height="wrap_content">
 
-    <ImageView
-        android:src="@drawable/ic_btn_round_minus"
-        android:id="@+id/delete"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_alignParentTop="true"
-        android:layout_alignParentRight="true"
-    />
-
     <TextView
         android:id="@+id/title"
         android:text="@string/contact"
         style="@style/record_title"
         android:layout_alignParentTop="true"
-        android:layout_toLeftOf="@+id/delete"
     />
 
     <LinearLayout
index 75fa953..4f5f559 100644 (file)
@@ -1,41 +1,57 @@
 <?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
+         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
+                    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.
+         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:orientation="vertical"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:singleLine="true"
-    />
+    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"
-    />
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <ImageView
+            android:id="@+id/active_tag_icon"
+            android:src="@drawable/active_tag_icon"
+            android:padding="4dip"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerVertical="true"
+            android:visibility="gone"
+            />
+
+        <TextView
+            android:id="@+id/title"
+            android:padding="4dip"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_toRightOf="@+id/active_tag_icon"
+            android:singleLine="true"
+            />
+    </RelativeLayout>
+
+    <TextView
+        android:id="@+id/date"
+        android:padding="4dip"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        />
 </LinearLayout>
 
diff --git a/res/layout/write_tag.xml b/res/layout/write_tag.xml
new file mode 100644 (file)
index 0000000..fc4ea53
--- /dev/null
@@ -0,0 +1,59 @@
+<?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:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+>
+    <TextView android:id="@+id/title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dip"
+
+        android:gravity="center_horizontal"
+        android:textAppearance="?android:attr/textAppearanceLarge"
+    />
+
+    <ImageView
+        android:layout_width="match_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1"
+
+        android:src="@drawable/ic_launcher_nfc"
+        android:gravity="center"
+        android:scaleType="center"
+    />
+
+    <TextView android:id="@+id/count"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+
+        android:gravity="center_horizontal"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+    />
+
+    <TextView android:id="@+id/status"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="16dip"
+
+        android:gravity="center_horizontal"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+    />
+</LinearLayout>
+
diff --git a/res/menu/my_tag_list_context_menu.xml b/res/menu/my_tag_list_context_menu.xml
new file mode 100644 (file)
index 0000000..f52abcc
--- /dev/null
@@ -0,0 +1,33 @@
+<?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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:id="@+id/set_as_active"
+        android:title="@string/menu_set_as_active"
+    />
+
+    <item android:id="@+id/edit"
+        android:title="@string/menu_edit"
+    />
+
+    <item android:id="@+id/delete"
+        android:title="@string/menu_delete"
+    />
+
+</menu>
+
+
index 572e047..9e61094 100644 (file)
@@ -67,7 +67,7 @@
     <string name="intro_text_more2" msgid="28835616891750572">"Para obtener más información, pulsa la tecla de menú y toca Ayuda en cualquier pantalla de la aplicación Tags."</string>
     <!-- outdated translation 7101932240859345016 -->     <string name="tag_deleted" msgid="5411251725532471624">"Etiqueta eliminada"</string>
     <string name="add_tag" msgid="703494598879855113">"Añadir nueva etiqueta"</string>
-    <string name="choose_my_tag" msgid="4208730132705026130">"Elige una etiqueta para compartir"</string>
+    <string name="choose_my_tag" msgid="4208730132705026130">"Elegir una etiqueta para compartir"</string>
     <string name="active_tag" msgid="7417732109196094058">"Etiqueta activa"</string>
     <string name="manage_my_tags" msgid="3616349610711109286">"Administrar mis etiquetas"</string>
 </resources>
index f3cc98d..5ef6a53 100644 (file)
@@ -64,6 +64,9 @@
     <!-- Label for control which enables/disables the "my tag" feature. -->
     <string name="turn_on_my_tag">Share my tag</string>
 
+    <!-- Label for activity which edits a tag to share/write [CHAR LIMIT=60] -->
+    <string name="edit_tag">Edit tag</string>
+
     <!-- Description for the control which enables/disables the "my tag" feature. -->
     <string name="turn_on_my_tag_subtitle">Allow others to read my tag </string>
 
@@ -182,4 +185,22 @@ Networks).</string>
     <!-- Header for list of tags that can be set as My tag. [CHAR LIMIT=50] -->
     <string name="manage_my_tags">Manage my tags</string>
 
+    <!-- Menu item indicating that the user wants to delete a tag. [CHAR LIMIT=50] -->
+    <string name="menu_delete">Delete tag</string>
+
+    <!-- Menu item indicating that the user wants to edit a tag. [CHAR LIMIT=50] -->
+    <string name="menu_edit">Edit tag</string>
+
+    <!-- Menu item indicating that the user wants to set a tag
+         as the active tag to share as the device's tag. [CHAR LIMIT=50] -->
+    <string name="menu_set_as_active">Set as active tag</string>
+
+    <!-- Error message displayed when attempting to enable sharing of a "My tag"
+         with no tag selected. [CHAR LIMIT=80] -->
+    <string name="no_tag_selected">You do not have a tag selected to share.</string>
+
+    <!-- Error message displayed when attempting to select an active tag when
+         none have been created. [CHAR LIMIT=80] -->
+    <string name="no_tags_created">You have no tags created.</string>
+
 </resources>
diff --git a/res/xml/filter_nfc.xml b/res/xml/filter_nfc.xml
new file mode 100644 (file)
index 0000000..c0916be
--- /dev/null
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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">
+    <!-- capture anything containing NDEF -->
+    <tech-list>
+        <tech>android.nfc.tech.Ndef</tech>
+    </tech-list>
+</resources>
index 50c4df9..edaff80 100644 (file)
 
 package com.android.apps.tag;
 
+import com.android.apps.tag.message.NdefMessageParser;
+import com.android.apps.tag.message.ParsedNdefMessage;
+import com.android.apps.tag.provider.TagContract.NdefMessages;
 import com.android.apps.tag.record.ImageRecord;
 import com.android.apps.tag.record.ParsedNdefRecord;
 import com.android.apps.tag.record.RecordEditInfo;
 import com.android.apps.tag.record.RecordEditInfo.EditCallbacks;
 import com.android.apps.tag.record.UriRecord;
 import com.android.apps.tag.record.VCardRecord;
-import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Lists;
 
 import android.app.Activity;
-import android.app.Dialog;
 import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.nfc.FormatException;
 import android.nfc.NdefMessage;
 import android.nfc.NdefRecord;
+import android.os.AsyncTask;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
-import java.util.ArrayList;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
 import java.util.Set;
 
 /**
  * A base {@link Activity} class for an editor of an {@link NdefMessage} tag.
  *
- * The core of the editing is done by various child {@link View}s that differ based on
- * {@link ParsedNdefRecord} types. Each type of {@link ParsedNdefRecord} can build views to
+ * The core of the editing is done by various a {@link View}s that differs based on
+ * {@link ParsedNdefRecord} types. Each type of {@link ParsedNdefRecord} can build a to
  * pick/select a new piece of content, or edit an existing content for the {@link NdefMessage}.
  */
-public abstract class EditTagActivity extends Activity implements OnClickListener, EditCallbacks {
+public class EditTagActivity extends Activity implements OnClickListener, EditCallbacks {
+
+    private static final String LOG_TAG = "Tags";
 
-    private static final String BUNDLE_KEY_OUTSTANDING_PICK = "outstanding-pick";
-    protected static final int DIALOG_ID_ADD_CONTENT = 0;
+    protected static final String BUNDLE_KEY_RECORD = "outstanding-pick";
+    public static final String EXTRA_RESULT_MSG = "com.android.apps.tag.msg";
+    public static final String EXTRA_NEW_RECORD_INFO = "com.android.apps.tag.new_record";
 
-    private static final Set<String> SUPPORTED_RECORD_TYPES = ImmutableSet.of(
-        ImageRecord.RECORD_TYPE,
-        UriRecord.RECORD_TYPE,
-        VCardRecord.RECORD_TYPE
+    protected static final Set<String> SUPPORTED_RECORD_TYPES = ImmutableSet.of(
+        VCardRecord.RECORD_TYPE,
+        UriRecord.RECORD_TYPE
     );
 
     /**
-     * Records contained in the current message being edited.
+     * The underlying record in the tag being edited.
      */
-    protected final ArrayList<RecordEditInfo> mRecords = Lists.newArrayList();
+    private RecordEditInfo mRecord;
 
     /**
      * The container where the subviews for each record are housed.
@@ -68,9 +80,10 @@ public abstract class EditTagActivity extends Activity implements OnClickListene
     private ViewGroup mContentRoot;
 
     /**
-     * Info about an outstanding picking activity to add a new record.
+     * Whether or not data was already parsed from an {@link Intent}. This happens when the user
+     * shares data via the My tag feature.
      */
-    private RecordEditInfo mRecordWithOutstandingPick;
+    private boolean mParsedIntent = false;
 
     private LayoutInflater mInflater;
 
@@ -78,17 +91,21 @@ public abstract class EditTagActivity extends Activity implements OnClickListene
     protected void onCreate(Bundle savedState) {
         super.onCreate(savedState);
 
-        if (savedState != null) {
-            mRecordWithOutstandingPick = savedState.getParcelable(BUNDLE_KEY_OUTSTANDING_PICK);
-        }
+        setContentView(R.layout.edit_tag_activity);
+        setTitle(getResources().getString(R.string.edit_tag));
+
         mInflater = LayoutInflater.from(this);
-    }
+        findViewById(R.id.save).setOnClickListener(this);
+        findViewById(R.id.cancel).setOnClickListener(this);
 
-    protected ViewGroup getContentRoot() {
-        if (mContentRoot == null) {
-            mContentRoot = (ViewGroup) findViewById(R.id.content_parent);
+        mContentRoot = (ViewGroup) findViewById(R.id.content_parent);
+
+        if (savedState != null) {
+            mRecord = savedState.getParcelable(BUNDLE_KEY_RECORD);
+            refresh();
+        } else {
+            resolveIntent();
         }
-        return mContentRoot;
     }
 
     /**
@@ -100,151 +117,238 @@ public abstract class EditTagActivity extends Activity implements OnClickListene
     }
 
     /**
-     * Builds a {@link View} used as an item in a list when picking a new piece of content to add
-     * to the tag.
+     * Builds a snapshot of current value as held in the internal state of this editor.
      */
-    public View getAddView(ViewGroup parent, String type) {
-        if (ImageRecord.RECORD_TYPE.equals(type)) {
-            return ImageRecord.getAddView(this, mInflater, parent);
-        } else if (UriRecord.RECORD_TYPE.equals(type)) {
-            return UriRecord.getAddView(this, mInflater, parent);
-        } else if (VCardRecord.RECORD_TYPE.equals(type)) {
-            return VCardRecord.getAddView(this, mInflater, parent);
-        }
-        throw new IllegalArgumentException("Not a supported view type");
+    public NdefRecord getValue() {
+        return mRecord.getValue();
     }
 
     /**
-     * Builds a snapshot of current values as held in the internal state of this editor.
+     * Refreshes the UI with updated content from the record.
+     * Typically used when the records require an external {@link Activity} to edit.
      */
-    public ArrayList<NdefRecord> getValues() {
-        ArrayList<NdefRecord> result = new ArrayList<NdefRecord>(mRecords.size());
-        for (RecordEditInfo editInfo : mRecords) {
-            result.add(editInfo.getValue());
-        }
-        return result;
+    public void refresh() {
+        ViewGroup root = mContentRoot;
+        View editView = mRecord.getEditView(this, mInflater, root, this);
+        root.removeAllViews();
+        root.addView(editView);
     }
 
-    /**
-     * Builds a {@link View} used as an item in a list when editing content for a tag.
-     */
-    public void addRecord(RecordEditInfo editInfo) {
-        mRecords.add(Preconditions.checkNotNull(editInfo));
-        addViewForRecord(editInfo);
-    }
 
-    /**
-     * Adds a child editor view for a record.
-     */
-    public void addViewForRecord(RecordEditInfo editInfo) {
-        ViewGroup root = getContentRoot();
-        View editView = editInfo.getEditView(this, mInflater, root, this);
-        root.addView(mInflater.inflate(R.layout.tag_divider, root, false));
-        root.addView(editView);
+    @Override
+    public void startPickForRecord(RecordEditInfo editInfo, Intent intent) {
+        startActivityForResult(intent, 0);
     }
 
-    protected void rebuildChildViews() {
-        ViewGroup root = getContentRoot();
-        root.removeAllViews();
-        for (RecordEditInfo editInfo : mRecords) {
-            addViewForRecord(editInfo);
-        }
+    @Override
+    public void deleteRecord(RecordEditInfo editInfo) {
     }
 
     @Override
-    protected Dialog onCreateDialog(int id, Bundle args) {
-        if (id == DIALOG_ID_ADD_CONTENT) {
-            return new TagContentSelector(this);
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if ((resultCode != RESULT_OK) || (data == null)) {
+            return;
         }
-        return super.onCreateDialog(id, args);
-    }
 
-    @Override
-    protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
-        super.onPrepareDialog(id, dialog, args);
-        if (dialog instanceof TagContentSelector) {
-            ((TagContentSelector) dialog).rebuildViews();
+        // Handles results from another Activity that picked content to write to a tag.
+        try {
+            mRecord.handlePickResult(this, data);
+        } catch (IllegalArgumentException ex) {
+            // TODO: handle.
+            return;
         }
+        refresh();
     }
 
-    /**
-     * Displays a {@link Dialog} to select a new content type to add to the Tag.
-     */
-    protected void showAddContentDialog() {
-        showDialog(DIALOG_ID_ADD_CONTENT);
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putParcelable(BUNDLE_KEY_RECORD, mRecord);
     }
 
-    @Override
-    public void startPickForRecord(RecordEditInfo editInfo, Intent intent) {
-        mRecordWithOutstandingPick = editInfo;
-        startActivityForResult(intent, 0);
+    interface GetTagQuery {
+        final static String[] PROJECTION = new String[] {
+                NdefMessages.BYTES
+        };
+
+        static final int COLUMN_BYTES = 0;
     }
 
     /**
-     * Handles a click to select and add a new content type.
+     * Loads a tag from the database, parses it, and builds the views.
      */
-    public void onAddContentClick(View target) {
-        Object tag = target.getTag();
-        if ((tag == null) || !(tag instanceof RecordEditInfo)) {
+    final class LoadTagTask extends AsyncTask<Uri, Void, Cursor> {
+        @Override
+        public Cursor doInBackground(Uri... args) {
+            Cursor cursor = getContentResolver().query(args[0], GetTagQuery.PROJECTION,
+                    null, null, null);
+
+            // Ensure the cursor loads its window.
+            if (cursor != null) {
+                cursor.getCount();
+            }
+            return cursor;
+        }
+
+        @Override
+        public void onPostExecute(Cursor cursor) {
+            NdefMessage msg = null;
+            try {
+                if (cursor != null && cursor.moveToFirst()) {
+                    msg = new NdefMessage(cursor.getBlob(GetTagQuery.COLUMN_BYTES));
+                    if (msg != null) {
+                        populateFromMessage(msg);
+                    } else {
+                        // TODO: do something more graceful.
+                        finish();
+                    }
+                }
+            } catch (FormatException e) {
+                Log.e(LOG_TAG, "Unable to parse tag for editing.", e);
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+        }
+    }
+
+    protected void resolveIntent() {
+        Intent intent = getIntent();
+
+        if (Intent.ACTION_SEND.equals(intent.getAction()) && !mParsedIntent) {
+            if (buildFromSendIntent(intent)) {
+                return;
+            }
+
+            mParsedIntent = true;
             return;
         }
 
-        RecordEditInfo info = (RecordEditInfo) tag;
-        Intent pickIntent = info.getPickIntent();
-        if (pickIntent != null) {
-            startPickForRecord(info, pickIntent);
+        Uri uri = intent.getData();
+        if (uri != null) {
+            // Edit existing tag.
+            new LoadTagTask().execute(uri);
         } else {
-            // Does not require an external Activity. Add the edit view directly.
-            addRecord(info);
+            RecordEditInfo newRecord = intent.getParcelableExtra(EXTRA_NEW_RECORD_INFO);
+            if (newRecord != null) {
+                initializeForNewTag(newRecord);
+            }
         }
     }
 
-    @Override
-    public void deleteRecord(RecordEditInfo editInfo) {
-        mRecords.remove(editInfo);
-        rebuildChildViews();
-    }
+    private void initializeForNewTag(RecordEditInfo editInfo) {
+        mRecord = editInfo;
 
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        if ((resultCode != RESULT_OK) || (data == null)) {
-            mRecordWithOutstandingPick = null;
-            return;
+        Intent pickIntent = editInfo.getPickIntent();
+        if (pickIntent != null) {
+            startPickForRecord(editInfo, pickIntent);
+        } else {
+            refresh();
         }
-        if (mRecordWithOutstandingPick == null) {
+    }
+
+    private void populateFromMessage(NdefMessage refMessage) {
+        // Locally stored message.
+        ParsedNdefMessage parsed = NdefMessageParser.parse(refMessage);
+        List<ParsedNdefRecord> records = parsed.getRecords();
+
+        // TODO: loosen this restriction.
+        // There is always a "Text" record for a My Tag.
+        if (records.size() != 1) {
+            Log.w(LOG_TAG, "Message not in expected format");
             return;
         }
+        mRecord = records.get(0).getEditInfo(this);
+        refresh();
+    }
 
-        // Handles results from another Activity that picked content to write to a tag.
-        RecordEditInfo recordInfo = mRecordWithOutstandingPick;
-        try {
-            recordInfo.handlePickResult(this, data);
-        } catch (IllegalArgumentException ex) {
-            if (mRecords.contains(recordInfo)) {
-                deleteRecord(recordInfo);
+    /**
+     * Populates the editor from extras in a given {@link Intent}
+     * @param intent the {@link Intent} to parse.
+     * @return whether or not the {@link Intent} could be handled.
+     */
+    private boolean buildFromSendIntent(final Intent intent) {
+        String type = intent.getType();
+
+        if ("text/plain".equals(type)) {
+            String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
+            try {
+                URL parsed = new URL(text);
+
+                // Valid URL.
+                mRecord = new UriRecord.UriRecordEditInfo(text);
+                refresh();
+                return true;
+
+            } catch (MalformedURLException ex) {
+                // TODO: handle.
+            }
+
+        } else if ("text/x-vcard".equals(type)) {
+            Uri stream = (Uri) getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
+            if (stream != null) {
+                RecordEditInfo editInfo = VCardRecord.editInfoForUri(stream);
+                if (editInfo != null) {
+                    mRecord = editInfo;
+                    refresh();
+                    return true;
+                }
             }
-            return;
         }
 
-        if (mRecords.contains(recordInfo)) {
-            // Editing an existing record. Just rebuild everything.
-            rebuildChildViews();
+        return false;
+    }
+
+    /**
+     * Saves the content of the tag.
+     */
+    private void saveAndFinish() {
+        NdefMessage msg = new NdefMessage(new NdefRecord[] { mRecord.getValue() });
+
+        if (Intent.ACTION_SEND.equals(getIntent().getAction())) {
+            // If opening directly from a different application via ACTION_SEND, save the tag and
+            // open the MyTagList so they can enable it.
+            Intent openMyTags = new Intent(this, MyTagList.class);
+            openMyTags.putExtra(EXTRA_RESULT_MSG, msg);
+            startActivity(openMyTags);
+            finish();
 
         } else {
-            // Adding a new record.
-            addRecord(recordInfo);
+            Intent result = new Intent();
+            result.putExtra(EXTRA_RESULT_MSG, msg);
+            setResult(RESULT_OK, result);
+            finish();
         }
-        // TODO: handle errors in picking (e.g. the image is too big, etc).
+    }
 
-        mRecordWithOutstandingPick = null;
+    @Override
+    public void onClick(View target) {
+        switch (target.getId()) {
+            case R.id.save:
+                saveAndFinish();
+                break;
+            case R.id.cancel:
+                finish();
+                break;
+        }
     }
 
     @Override
-    protected void onSaveInstanceState(Bundle outState) {
-        super.onSaveInstanceState(outState);
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.menu, menu);
+        return true;
+    }
 
-        if (mRecordWithOutstandingPick != null) {
-            outState.putParcelable(BUNDLE_KEY_OUTSTANDING_PICK, mRecordWithOutstandingPick);
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.help:
+                HelpUtils.openHelp(this);
+                return true;
+
+            default:
+                return super.onOptionsItemSelected(item);
         }
     }
 }
diff --git a/src/com/android/apps/tag/MyTagActivity.java b/src/com/android/apps/tag/MyTagActivity.java
deleted file mode 100644 (file)
index 0ba621d..0000000
+++ /dev/null
@@ -1,233 +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 com.android.apps.tag.message.NdefMessageParser;
-import com.android.apps.tag.message.ParsedNdefMessage;
-import com.android.apps.tag.record.ParsedNdefRecord;
-import com.android.apps.tag.record.RecordEditInfo;
-import com.android.apps.tag.record.TextRecord;
-import com.android.apps.tag.record.UriRecord;
-import com.android.apps.tag.record.VCardRecord;
-import com.google.common.collect.Lists;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.net.Uri;
-import android.nfc.NdefMessage;
-import android.nfc.NdefRecord;
-import android.nfc.NfcAdapter;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.CheckBox;
-import android.widget.EditText;
-
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-
-/**
- * Editor {@link Activity} for the tag that can be programmed into the device.
- */
-public class MyTagActivity extends EditTagActivity implements OnClickListener {
-
-    private static final String LOG_TAG = "TagEditor";
-
-    private EditText mTextView;
-    private CheckBox mEnabled;
-
-    /**
-     * Whether or not data was already parsed from an {@link Intent}. This happens when the user
-     * shares data via the My tag feature.
-     */
-    private boolean mParsedIntent = false;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.my_tag_activity);
-
-        findViewById(R.id.toggle_enabled_target).setOnClickListener(this);
-        findViewById(R.id.add_content_target).setOnClickListener(this);
-
-        mTextView = (EditText) findViewById(R.id.input_tag_text);
-        mEnabled = (CheckBox) findViewById(R.id.toggle_enabled_checkbox);
-
-        populateEditor();
-    }
-
-    private void populateEditor() {
-        NdefMessage localMessage = NfcAdapter.getDefaultAdapter(this).getLocalNdefMessage();
-
-        if (Intent.ACTION_SEND.equals(getIntent().getAction()) && !mParsedIntent) {
-            if (localMessage != null) {
-                // TODO: prompt user for confirmation about wiping their old tag.
-            }
-
-            if (buildFromIntent(getIntent())) {
-                return;
-            }
-
-            mParsedIntent = true;
-
-        } else if (localMessage == null) {
-            mEnabled.setChecked(false);
-            return;
-
-        } else {
-            // Locally stored message.
-            ParsedNdefMessage parsed = NdefMessageParser.parse(localMessage);
-            List<ParsedNdefRecord> records = parsed.getRecords();
-
-            // There is always a "Text" record for a My Tag.
-            if (records.size() < 1) {
-                Log.w(LOG_TAG, "Local record not in expected format");
-                return;
-            }
-            mEnabled.setChecked(true);
-            mTextView.setText(((TextRecord) records.get(0)).getText());
-
-            mRecords.clear();
-            for (int i = 1, len = records.size(); i < len; i++) {
-                RecordEditInfo editInfo = records.get(i).getEditInfo(this);
-                if (editInfo != null) {
-                    addRecord(editInfo);
-                }
-            }
-            rebuildChildViews();
-        }
-    }
-
-    /**
-     * Populates the editor from extras in a given {@link Intent}
-     * @param intent the {@link Intent} to parse.
-     * @return whether or not the {@link Intent} could be handled.
-     */
-    private boolean buildFromIntent(final Intent intent) {
-        String type = intent.getType();
-
-        if ("text/plain".equals(type)) {
-            String title = getIntent().getStringExtra(Intent.EXTRA_SUBJECT);
-            mTextView.setText((title == null) ? "" : title);
-
-            String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
-            try {
-                URL parsed = new URL(text);
-
-                // Valid URL.
-                mTextView.setText("");
-                mRecords.add(new UriRecord.UriRecordEditInfo(text));
-                rebuildChildViews();
-
-            } catch (MalformedURLException ex) {
-                // Ignore. Just treat as plain text.
-                mTextView.setText((text == null) ? "" : text);
-            }
-
-            mEnabled.setChecked(true);
-            onSave();
-            return true;
-
-        } else if ("text/x-vcard".equals(type)) {
-            Uri stream = (Uri) getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
-            if (stream != null) {
-                RecordEditInfo editInfo = VCardRecord.editInfoForUri(stream);
-                if (editInfo != null) {
-                    mRecords.add(editInfo);
-                    rebuildChildViews();
-                }
-            }
-        }
-        // TODO: handle images.
-        return false;
-    }
-
-    /**
-     * Persists content to store.
-     */
-    private void onSave() {
-        String text = mTextView.getText().toString();
-        NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
-
-        if (!mEnabled.isChecked()) {
-            nfc.setLocalNdefMessage(null);
-            return;
-        }
-
-        Locale locale = getResources().getConfiguration().locale;
-        ArrayList<NdefRecord> values = Lists.newArrayList(
-                TextRecord.newTextRecord(text, locale)
-        );
-
-        values.addAll(getValues());
-
-        Log.d(LOG_TAG, "Writing local NdefMessage from tag app....");
-        nfc.setLocalNdefMessage(new NdefMessage(values.toArray(new NdefRecord[values.size()])));
-    }
-
-    @Override
-    public void onPause() {
-        super.onPause();
-        onSave();
-    }
-
-    @Override
-    public void onClick(View target) {
-        switch (target.getId()) {
-            case R.id.toggle_enabled_target:
-                boolean enabled = !mEnabled.isChecked();
-                mEnabled.setChecked(enabled);
-
-                // TODO: Persist to some store.
-                if (enabled) {
-                    onSave();
-                } else {
-                    NfcAdapter.getDefaultAdapter(this).setLocalNdefMessage(null);
-                }
-                break;
-
-            case R.id.add_content_target:
-                showAddContentDialog();
-                break;
-        }
-    }
-
-    @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        getMenuInflater().inflate(R.menu.menu, menu);
-        return true;
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        switch (item.getItemId()) {
-            case R.id.help:
-                HelpUtils.openHelp(this);
-                return true;
-
-            default:
-                return super.onOptionsItemSelected(item);
-        }
-    }
-}
index f22f4b1..3ba03c8 100644 (file)
 
 package com.android.apps.tag;
 
+import com.android.apps.tag.TagContentSelector.SelectContentCallbacks;
 import com.android.apps.tag.provider.TagContract.NdefMessages;
+import com.android.apps.tag.record.RecordEditInfo;
+import com.android.apps.tag.record.UriRecord;
+import com.android.apps.tag.record.VCardRecord;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 
 import android.app.Activity;
@@ -26,10 +31,11 @@ import android.app.Dialog;
 import android.content.ContentUris;
 import android.content.Context;
 import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.database.CharArrayBuffer;
 import android.database.Cursor;
+import android.net.Uri;
 import android.nfc.FormatException;
 import android.nfc.NdefMessage;
 import android.nfc.NfcAdapter;
@@ -39,14 +45,18 @@ import android.os.Bundle;
 import android.util.Log;
 import android.view.ContextMenu;
 import android.view.ContextMenu.ContextMenuInfo;
+import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
+import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.CheckBox;
 import android.widget.CursorAdapter;
+import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.SimpleAdapter;
 import android.widget.TextView;
@@ -55,22 +65,28 @@ import android.widget.Toast;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Set;
 
 /**
  * Displays the list of tags that can be set as "My tag", and allows the user to select the
  * active tag that the device shares.
  */
-public class MyTagList extends Activity implements OnItemClickListener, View.OnClickListener {
+public class MyTagList
+        extends Activity
+        implements OnItemClickListener, View.OnClickListener,
+                   SelectContentCallbacks, TagService.SaveCallbacks {
 
     static final String TAG = "TagList";
 
     private static final int REQUEST_EDIT = 0;
     private static final int DIALOG_ID_SELECT_ACTIVE_TAG = 0;
+    private static final int DIALOG_ID_ADD_NEW_TAG = 1;
 
     private static final String BUNDLE_KEY_TAG_ID_IN_EDIT = "tag-edit";
     private static final String PREF_KEY_ACTIVE_TAG = "active-my-tag";
     static final String PREF_KEY_TAG_TO_WRITE = "tag-to-write";
 
+
     private View mSelectActiveTagAnchor;
     private View mActiveTagDetails;
     private CheckBox mEnabled;
@@ -78,11 +94,12 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
 
     private TagAdapter mAdapter;
     private long mActiveTagId;
+    private Uri mTagBeingSaved;
     private NdefMessage mActiveTag;
-    private boolean mInitialLoadComplete = false;
 
     private WeakReference<SelectActiveTagDialog> mSelectActiveTagDialog;
     private long mTagIdInEdit = -1;
+    private long mTagIdLongPressed;
 
     private boolean mWriteSupport = false;
 
@@ -109,7 +126,7 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
 
         mActiveTagId = getPreferences(Context.MODE_PRIVATE).getLong(PREF_KEY_ACTIVE_TAG, -1);
 
-        // Setup the list
+        // Setup the list.
         mAdapter = new TagAdapter(this);
         mList = (ListView) findViewById(android.R.id.list);
         mList.setAdapter(mAdapter);
@@ -130,6 +147,12 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
             mWriteSupport = true;
             registerForContextMenu(mList);
         }
+
+        if (getIntent().hasExtra(EditTagActivity.EXTRA_RESULT_MSG)) {
+            NdefMessage msg = (NdefMessage) Preconditions.checkNotNull(
+                    getIntent().getParcelableExtra(EditTagActivity.EXTRA_RESULT_MSG));
+            saveNewMessage(msg);
+        }
     }
 
     @Override
@@ -153,29 +176,14 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
     }
 
     @Override
-    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
-        if (mWriteSupport) {
-            menu.add(0, 1, 0, "Write to next tag scanned");
-        }
-    }
-
-    @Override
-    public boolean onContextItemSelected(MenuItem item) {
-        AdapterView.AdapterContextMenuInfo info;
-        try {
-             info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
-        } catch (ClassCastException e) {
-            Log.e(TAG, "bad menuInfo", e);
-            return false;
-        }
-
-        SharedPreferences prefs = getSharedPreferences("tags.pref", Context.MODE_PRIVATE);
-        prefs.edit().putLong(PREF_KEY_TAG_TO_WRITE, info.id).apply();
-        return true;
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        editTag(id);
     }
 
-    @Override
-    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+    /**
+     * Opens the tag editor for a particular tag.
+     */
+    private void editTag(long id) {
         // TODO: use implicit Intent?
         Intent intent = new Intent(this, EditTagActivity.class);
         intent.setData(ContentUris.withAppendedId(NdefMessages.CONTENT_URI, id));
@@ -220,29 +228,22 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
 
         @Override
         protected void onPostExecute(Cursor cursor) {
-            boolean firstLoad = !mInitialLoadComplete;
-            if (!mInitialLoadComplete) {
-                mInitialLoadComplete = true;
-            }
+            mAdapter.changeCursor(cursor);
 
             if (cursor == null || cursor.getCount() == 0) {
                 setEmptyView();
-            } else if (mActiveTagId != -1) {
-                mAdapter.changeCursor(cursor);
-
+            } else {
                 // Find the active tag.
-                cursor.moveToPosition(-1);
-                while (cursor.moveToNext()) {
-                    if (mActiveTagId == cursor.getLong(TagQuery.COLUMN_ID)) {
-                        selectActiveTag(cursor.getPosition());
-
-                        // If there was an existing shared tag, we update the contents, since
-                        // the active tag contents may have been changed. This also forces the
-                        // active tag to be in sync with what the NfcAdapter.
-                        if (NfcAdapter.getDefaultAdapter().getLocalNdefMessage() != null) {
-                            enableSharing();
+                if (mTagBeingSaved != null) {
+                    selectTagBeingSaved(mTagBeingSaved);
+
+                } else if (mActiveTagId != -1) {
+                    cursor.moveToPosition(-1);
+                    while (cursor.moveToNext()) {
+                        if (mActiveTagId == cursor.getLong(TagQuery.COLUMN_ID)) {
+                            selectActiveTag(cursor.getPosition());
+                            break;
                         }
-                        break;
                     }
                 }
             }
@@ -262,6 +263,7 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
     static final class ViewHolder {
         public CharArrayBuffer titleBuffer;
         public TextView mainLine;
+        public ImageView activeIcon;
     }
 
     /**
@@ -282,6 +284,9 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
             CharArrayBuffer buf = holder.titleBuffer;
             cursor.copyStringToBuffer(TagQuery.COLUMN_TITLE, buf);
             holder.mainLine.setText(buf.data, 0, buf.sizeCopied);
+
+            boolean isActive = cursor.getLong(TagQuery.COLUMN_ID) == mActiveTagId;
+            holder.activeIcon.setVisibility(isActive ? View.VISIBLE : View.GONE);
         }
 
         @Override
@@ -292,6 +297,7 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
             ViewHolder holder = new ViewHolder();
             holder.titleBuffer = new CharArrayBuffer(64);
             holder.mainLine = (TextView) view.findViewById(R.id.title);
+            holder.activeIcon = (ImageView) view.findViewById(R.id.active_tag_icon);
             view.findViewById(R.id.date).setVisibility(View.GONE);
             view.setTag(holder);
 
@@ -312,13 +318,12 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
                 boolean enabled = !mEnabled.isChecked();
                 if (enabled) {
                     if (mActiveTag != null) {
-                        enableSharing();
+                        enableSharingAndStoreTag();
                         return;
                     }
-                    // TODO: just disable the checkbox when no tag is set
                     Toast.makeText(
                             this,
-                            "You must select a tag to share first.",
+                            getResources().getString(R.string.no_tag_selected),
                             Toast.LENGTH_SHORT).show();
                 }
 
@@ -326,18 +331,118 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
                 break;
 
             case R.id.add_tag:
-                // TODO: use implicit intents.
-                Intent intent = new Intent(this, EditTagActivity.class);
-                startActivityForResult(intent, REQUEST_EDIT);
+                showDialog(DIALOG_ID_ADD_NEW_TAG);
                 break;
 
             case R.id.active_tag:
+                if (mAdapter.getCursor() == null || mAdapter.getCursor().isClosed()) {
+                    // Hopefully shouldn't happen.
+                    return;
+                }
+
+                if (mAdapter.getCursor().getCount() == 0) {
+                    OnClickListener onAdd = new OnClickListener() {
+                        @Override
+                        public void onClick(DialogInterface dialog, int which) {
+                            if (which == AlertDialog.BUTTON_POSITIVE) {
+                                showDialog(DIALOG_ID_ADD_NEW_TAG);
+                            }
+                        }
+                    };
+                    new AlertDialog.Builder(this)
+                            .setNegativeButton(android.R.string.cancel, null)
+                            .setPositiveButton(R.string.add_tag, onAdd)
+                            .setMessage(R.string.no_tags_created)
+                            .show();
+                    return;
+                }
                 showDialog(DIALOG_ID_SELECT_ACTIVE_TAG);
                 break;
         }
     }
 
     @Override
+    public Set<String> getSupportedTypes() {
+        return ImmutableSet.of(
+                VCardRecord.RECORD_TYPE,
+                UriRecord.RECORD_TYPE
+        );
+    }
+
+    @Override
+    public void onSelectContent(RecordEditInfo info) {
+        Intent intent = new Intent(this, EditTagActivity.class);
+        intent.putExtra(EditTagActivity.EXTRA_NEW_RECORD_INFO, info);
+        startActivityForResult(intent, REQUEST_EDIT);
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) {
+        Cursor cursor = mAdapter.getCursor();
+        if (cursor == null
+                || cursor.isClosed()
+                || !cursor.moveToPosition(((AdapterContextMenuInfo) info).position)) {
+            return;
+        }
+
+        menu.setHeaderTitle(cursor.getString(TagQuery.COLUMN_TITLE));
+        long id = cursor.getLong(TagQuery.COLUMN_ID);
+        MenuInflater inflater = getMenuInflater();
+        inflater.inflate(R.menu.my_tag_list_context_menu, menu);
+
+        // Prepare the menu for the item.
+        menu.findItem(R.id.set_as_active).setVisible(id != mActiveTagId);
+        mTagIdLongPressed = id;
+
+        if (mWriteSupport) {
+            menu.add(0, 1, 0, "Write to tag");
+        }
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        long id = mTagIdLongPressed;
+        switch (item.getItemId()) {
+            case R.id.delete:
+                deleteTag(id);
+                return true;
+
+            case R.id.set_as_active:
+                Cursor cursor = mAdapter.getCursor();
+                if (cursor == null || cursor.isClosed()) {
+                    break;
+                }
+
+                for (int position = 0; cursor.moveToPosition(position); position++) {
+                    if (cursor.getLong(TagQuery.COLUMN_ID) == id) {
+                        selectActiveTag(position);
+                        return true;
+                    }
+                }
+                break;
+
+            case R.id.edit:
+                editTag(id);
+                return true;
+
+            case 1:
+                AdapterView.AdapterContextMenuInfo info;
+                try {
+                    info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+                } catch (ClassCastException e) {
+                    Log.e(TAG, "bad menuInfo", e);
+                    break;
+                }
+
+                Intent intent = new Intent(this, WriteTagActivity.class);
+                intent.putExtra("id", info.id);
+                startActivity(intent);
+                return true;
+        }
+        return false;
+    }
+
+    @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         if (requestCode == REQUEST_EDIT && resultCode == RESULT_OK) {
             NdefMessage msg = (NdefMessage) Preconditions.checkNotNull(
@@ -346,17 +451,36 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
             if (mTagIdInEdit != -1) {
                 TagService.updateMyMessage(this, mTagIdInEdit, msg);
             } else {
-                TagService.saveMyMessages(this, new NdefMessage[] { msg });
+                saveNewMessage(msg);
             }
         }
     }
 
+    private void saveNewMessage(NdefMessage msg) {
+        TagService.saveMyMessage(this, msg, this);
+    }
+
+    @Override
+    public void onSaveComplete(Uri newMsgUri) {
+        if (isFinishing()) {
+            // Callback came asynchronously and was after we finished - ignore.
+            return;
+        }
+        mTagBeingSaved = newMsgUri;
+        selectTagBeingSaved(newMsgUri);
+    }
+
     @Override
     protected Dialog onCreateDialog(int id, Bundle args) {
         if (id == DIALOG_ID_SELECT_ACTIVE_TAG) {
-            mSelectActiveTagDialog = new WeakReference<SelectActiveTagDialog>(
-                    new SelectActiveTagDialog(this, mAdapter.getCursor()));
-            return mSelectActiveTagDialog.get();
+            Context lightTheme = new ContextThemeWrapper(this, android.R.style.Theme_Light);
+            SelectActiveTagDialog dialog = new SelectActiveTagDialog(lightTheme,
+                    mAdapter.getCursor());
+            dialog.setInverseBackgroundForced(true);
+            mSelectActiveTagDialog = new WeakReference<SelectActiveTagDialog>(dialog);
+            return dialog;
+        } else if (id == DIALOG_ID_ADD_NEW_TAG) {
+            return new TagContentSelector(this, this);
         }
         return super.onCreateDialog(id, args);
     }
@@ -365,8 +489,8 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
      * Selects the tag to be used as the "My tag" shared tag.
      *
      * This does not necessarily persist the selection to the {@code NfcAdapter}. That must be done
-     * via {@link #enableSharing}. However, it will call {@link #disableSharing} if the tag
-     * is invalid.
+     * via {@link #enableSharingAndStoreTag()}. However, it will call {@link #disableSharing()}
+     * if the tag is invalid.
      */
     private void selectActiveTag(int position) {
         Cursor cursor = mAdapter.getCursor();
@@ -382,8 +506,15 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
                         .putLong(PREF_KEY_ACTIVE_TAG, mActiveTagId)
                         .apply();
 
-                // Notify NFC adapter of the My tag contents.
                 updateActiveTagView(cursor.getString(TagQuery.COLUMN_TITLE));
+                mAdapter.notifyDataSetChanged();
+
+                // If there was an existing shared tag, we update the contents, since
+                // the active tag contents may have been changed. This also forces the
+                // active tag to be in sync with what the NfcAdapter.
+                if (NfcAdapter.getDefaultAdapter(this).getLocalNdefMessage() != null) {
+                    enableSharingAndStoreTag();
+                }
 
             } catch (FormatException e) {
                 // TODO: handle.
@@ -393,16 +524,42 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
             updateActiveTagView(null);
             disableSharing();
         }
+        mTagBeingSaved = null;
+    }
+
+    /**
+     * Selects the tag to be used as the "My tag" shared tag, if the specified URI is found.
+     * If the URI is not found, the next load will attempt to look for a matching tag to select.
+     *
+     * Commonly used for new tags that was just added to the database, and may not yet be
+     * reflected in the {@code Cursor}.
+     */
+    private void selectTagBeingSaved(Uri uri) {
+        Cursor cursor = mAdapter.getCursor();
+        if (cursor == null) {
+            return;
+        }
+        cursor.moveToPosition(-1);
+        while (cursor.moveToNext()) {
+            Uri tagUri = ContentUris.withAppendedId(
+                    NdefMessages.CONTENT_URI,
+                    cursor.getLong(TagQuery.COLUMN_ID));
+            if (tagUri.equals(uri)) {
+                selectActiveTag(cursor.getPosition());
+                return;
+            }
+        }
     }
 
-    private void enableSharing() {
+    private void enableSharingAndStoreTag() {
         mEnabled.setChecked(true);
-        NfcAdapter.getDefaultAdapter().setLocalNdefMessage(Preconditions.checkNotNull(mActiveTag));
+        NfcAdapter.getDefaultAdapter(this).setLocalNdefMessage(
+                Preconditions.checkNotNull(mActiveTag));
     }
 
     private void disableSharing() {
         mEnabled.setChecked(false);
-        NfcAdapter.getDefaultAdapter().setLocalNdefMessage(null);
+        NfcAdapter.getDefaultAdapter(this).setLocalNdefMessage(null);
     }
 
     private void updateActiveTagView(String title) {
@@ -416,6 +573,16 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
         }
     }
 
+    /**
+     * Removes the tag from the "My tag" list.
+     */
+    private void deleteTag(long id) {
+        if (id == mActiveTagId) {
+            selectActiveTag(-1);
+        }
+        TagService.delete(this, ContentUris.withAppendedId(NdefMessages.CONTENT_URI, id));
+    }
+
     class SelectActiveTagDialog extends AlertDialog
             implements DialogInterface.OnClickListener, OnItemClickListener {
 
@@ -426,8 +593,7 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
             super(context);
 
             setTitle(context.getResources().getString(R.string.choose_my_tag));
-            LayoutInflater inflater = LayoutInflater.from(context);
-            ListView list = new ListView(MyTagList.this);
+            ListView list = new ListView(context);
 
             mData = Lists.newArrayList();
             mSelectAdapter = new SimpleAdapter(
@@ -474,7 +640,7 @@ public class MyTagList extends Activity implements OnItemClickListener, View.OnC
         @Override
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
             selectActiveTag(position);
-            enableSharing();
+            enableSharingAndStoreTag();
             cancel();
         }
     }
index a514946..cff0f90 100644 (file)
@@ -63,7 +63,7 @@ public class TagBrowserActivity extends TabActivity implements DialogInterface.O
         tabHost.addTab(tabHost.newTabSpec("mytag")
                 .setIndicator(getText(R.string.tab_my_tag),
                         res.getDrawable(R.drawable.ic_tab_my_tag))
-                .setContent(new Intent().setClass(this, MyTagActivity.class)));
+                .setContent(new Intent().setClass(this, MyTagList.class)));
 
         SharedPreferences preferences = getPreferences(Context.MODE_PRIVATE);
         if (!preferences.getBoolean(PREF_KEY_SHOW_INTRO, false)) {
index e1055a6..3de77fa 100644 (file)
 
 package com.android.apps.tag;
 
+import com.android.apps.tag.record.RecordEditInfo;
+import com.android.apps.tag.record.UriRecord;
+import com.android.apps.tag.record.VCardRecord;
+
+import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.content.DialogInterface;
@@ -24,6 +29,8 @@ import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
+import java.util.Set;
+
 /**
  * A {@link Dialog} that presents options to select data which can be written into a
  * {@link NdefRecord} for an NFC tag.
@@ -31,17 +38,31 @@ import android.view.ViewGroup;
 public class TagContentSelector extends AlertDialog
         implements DialogInterface.OnClickListener, android.view.View.OnClickListener {
 
-    private final EditTagActivity mActivity;
     private final ViewGroup mListRoot;
+    private final LayoutInflater mInflater;
+    private final SelectContentCallbacks mCallbacks;
+
+    public interface SelectContentCallbacks {
+        /**
+         * Determines which data types should be displayed in this selector.
+         * Keys correspond to types in {@link RecordEditInfo}.
+         */
+        Set<String> getSupportedTypes();
 
-    public TagContentSelector(EditTagActivity activity) {
+        /**
+         * Handle a selection of new data for an {@link NdefRecord}.
+         */
+        void onSelectContent(RecordEditInfo info);
+    }
+
+    public TagContentSelector(Activity activity, SelectContentCallbacks callbacks) {
         super(activity);
-        mActivity = activity;
+        mCallbacks = callbacks;
 
         setTitle(activity.getResources().getString(R.string.select_type));
 
-        LayoutInflater inflater = LayoutInflater.from(activity);
-        ViewGroup root = (ViewGroup) inflater.inflate(R.layout.tag_content_selector, null);
+        mInflater = LayoutInflater.from(activity);
+        ViewGroup root = (ViewGroup) mInflater.inflate(R.layout.tag_content_selector, null);
         mListRoot = (ViewGroup) root.findViewById(R.id.list);
 
         rebuildViews();
@@ -54,10 +75,24 @@ public class TagContentSelector extends AlertDialog
                 this);
     }
 
+    /**
+     * Builds a {@link View} used as an item in a list when picking a new piece of content to add
+     * to the tag.
+     */
+    public View getAddView(ViewGroup parent, String type) {
+        if (UriRecord.RECORD_TYPE.equals(type)) {
+            return UriRecord.getAddView(getContext(), mInflater, parent);
+        } else if (VCardRecord.RECORD_TYPE.equals(type)) {
+            return VCardRecord.getAddView(getContext(), mInflater, parent);
+        }
+        throw new IllegalArgumentException("Not a supported view type");
+    }
+
+
     public void rebuildViews() {
         mListRoot.removeAllViews();
-        for (String type : mActivity.getSupportedTypes()) {
-            View selectItemView = mActivity.getAddView(mListRoot, type);
+        for (String type : mCallbacks.getSupportedTypes()) {
+            View selectItemView = getAddView(mListRoot, type);
             if (selectItemView != null) {
                 selectItemView.setOnClickListener(this);
                 mListRoot.addView(selectItemView);
@@ -65,7 +100,6 @@ public class TagContentSelector extends AlertDialog
         }
     }
 
-
     @Override
     public void onClick(DialogInterface dialog, int which) {
         dismiss();
@@ -73,7 +107,11 @@ public class TagContentSelector extends AlertDialog
 
     @Override
     public void onClick(View target) {
-        mActivity.onAddContentClick(target);
+        Object tag = target.getTag();
+        if ((tag == null) || !(tag instanceof RecordEditInfo)) {
+            return;
+        }
+        mCallbacks.onSelectContent((RecordEditInfo) tag);
         dismiss();
     }
 }
index f3d9c82..76f45a6 100644 (file)
@@ -61,7 +61,6 @@ public class TagList extends ListActivity implements OnClickListener {
         new TagLoaderTask().execute((Void[]) null);
         mAdapter = new TagAdapter(this);
         setListAdapter(mAdapter);
-        registerForContextMenu(getListView());
     }
 
     @Override
index 5847bc2..bca13b6 100644 (file)
@@ -21,11 +21,15 @@ import com.android.apps.tag.provider.TagContract.NdefMessages;
 import android.app.IntentService;
 import android.app.PendingIntent;
 import android.app.PendingIntent.CanceledException;
+import android.app.Service;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.nfc.NdefMessage;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.IBinder;
 import android.os.Parcelable;
 import android.util.Log;
 
@@ -47,6 +51,18 @@ public class TagService extends IntentService {
         super("SaveTagService");
     }
 
+    public interface SaveCallbacks {
+        void onSaveComplete(Uri uri);
+    }
+
+    private static final class EmptyService extends Service {
+        @Override
+        public IBinder onBind(Intent intent) {
+            return null;
+        }
+    }
+
+
     @Override
     public void onHandleIntent(Intent intent) {
         if (intent.hasExtra(EXTRA_SAVE_MSGS)) {
@@ -115,13 +131,53 @@ public class TagService extends IntentService {
         context.startService(intent);
     }
 
-    public static void saveMyMessages(Context context, NdefMessage[] msgs) {
+    public static void saveMyMessages(Context context, NdefMessage[] msgs, PendingIntent pending) {
         Intent intent = new Intent(context, TagService.class);
         intent.putExtra(TagService.EXTRA_SAVE_MSGS, msgs);
         intent.putExtra(TagService.EXTRA_SAVE_IN_MY_TAGS, true);
+        if (pending != null) {
+            intent.putExtra(TagService.EXTRA_PENDING_INTENT, pending);
+        }
         context.startService(intent);
     }
 
+    public static void saveMyMessage(
+            final Context context, final NdefMessage msg, final SaveCallbacks callbacks) {
+        final Handler handler = new Handler();
+        Thread thread = new Thread() {
+            @Override
+            public void run() {
+                // Start service to ensure the save completes in case this app gets thrown into the
+                // background.
+                context.startService(new Intent(context, EmptyService.class));
+
+
+                ContentValues values = NdefMessages.toValues(
+                        context, msg,
+                        false /* starred */, true /* is one of "my tags" */,
+                        System.currentTimeMillis());
+
+                // Start dummy service to ensure the save completes.
+                context.startService(new Intent(context, EmptyService.class));
+
+                final Uri result =
+                        context.getContentResolver().insert(NdefMessages.CONTENT_URI, values);
+                handler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callbacks.onSaveComplete(result);
+                    }
+                });
+
+                // Stop service so we can be killed.
+                context.stopService(new Intent(context, EmptyService.class));
+            }
+        };
+        thread.setPriority(Thread.MIN_PRIORITY);
+        thread.start();
+    }
+
+
     public static void updateMyMessage(Context context, long id, NdefMessage msg) {
         Intent intent = new Intent(context, TagService.class);
         intent.putExtra(TagService.EXTRA_SAVE_MSGS, new NdefMessage[] { msg });
index e0af182..fd3fe2e 100644 (file)
@@ -39,9 +39,8 @@ import android.nfc.NdefMessage;
 import android.nfc.NdefRecord;
 import android.nfc.NfcAdapter;
 import android.nfc.Tag;
-import android.nfc.technology.Ndef;
-import android.nfc.technology.NdefFormatable;
-import android.nfc.technology.TagTechnology;
+import android.nfc.tech.Ndef;
+import android.nfc.tech.NdefFormatable;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Parcelable;
@@ -184,18 +183,6 @@ public class TagViewer extends Activity implements OnClickListener {
                 }
             }
 
-            // Check to see if there's a tag queued up for writing.
-            SharedPreferences prefs = getSharedPreferences("tags.pref", Context.MODE_PRIVATE);
-            long tagToWrite = prefs.getLong(MyTagList.PREF_KEY_TAG_TO_WRITE, 0);
-            prefs.edit().putLong(MyTagList.PREF_KEY_TAG_TO_WRITE, 0).apply();
-            if (tagToWrite != 0) {
-                if (writeTag((Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG), tagToWrite)) {
-                    Toast.makeText(this, "Tag written", Toast.LENGTH_SHORT).show();
-                    finish();
-                    return;
-                }
-            }
-
             // When a tag is discovered we send it to the service to be save. We
             // include a PendingIntent for the service to call back onto. This
             // will cause this activity to be restarted with onNewIntent(). At
@@ -270,53 +257,6 @@ public class TagViewer extends Activity implements OnClickListener {
         }
     }
 
-    private boolean writeTag(Tag tag, long id) {
-        try {
-            NfcAdapter adapter = NfcAdapter.getDefaultAdapter(this);
-            Cursor cursor = getContentResolver().query(
-                    ContentUris.withAppendedId(NdefMessages.CONTENT_URI, id),
-                    new String[] { NdefMessages.BYTES }, null, null, null);
-            if (cursor == null || !cursor.moveToFirst()) {
-                return false;
-            }
-
-            byte[] bytes = cursor.getBlob(0);
-            cursor.close();
-            NdefMessage msg = new NdefMessage(bytes);
-
-            Ndef ndef = (Ndef) tag.getTechnology(adapter, TagTechnology.NDEF);
-            if (ndef != null) {
-                ndef.connect();
-                if (!ndef.isWritable()) {
-                    Toast.makeText(this, "Tag is read-only, not writing", Toast.LENGTH_SHORT)
-                            .show();
-                    return false;
-                }
-                ndef.writeNdefMessage(msg);
-                Toast.makeText(this, "Wrote message to pre-formatted tag", Toast.LENGTH_SHORT)
-                        .show();
-                return true;
-            } else {
-                NdefFormatable format = (NdefFormatable) tag.getTechnology(adapter,
-                        TagTechnology.NDEF_FORMATABLE);
-                if (format != null) {
-                    format.connect();
-                    format.format(msg);
-                    Toast.makeText(this, "Formatted tag and wrote message", Toast.LENGTH_SHORT)
-                            .show();
-                    return true;
-                }
-            }
-        } catch (Exception e) {
-            Log.e(TAG, "Failed to write tag", e);
-        }
-
-        Toast.makeText(this, "Failed to write tag", Toast.LENGTH_SHORT)
-                .show();
-
-        return false;
-    }
-
     void buildTagViews(NdefMessage[] msgs) {
         if (msgs == null || msgs.length == 0) {
             return;
diff --git a/src/com/android/apps/tag/WriteTagActivity.java b/src/com/android/apps/tag/WriteTagActivity.java
new file mode 100644 (file)
index 0000000..11ed9e1
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2011 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.provider.TagContract.NdefMessages;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.Cursor;
+import android.graphics.Color;
+import android.nfc.FormatException;
+import android.nfc.NdefMessage;
+import android.nfc.NfcAdapter;
+import android.nfc.Tag;
+import android.nfc.tech.Ndef;
+import android.nfc.tech.NdefFormatable;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.TextView;
+
+import java.io.IOException;
+
+public class WriteTagActivity extends Activity {
+    static final String TAG = WriteTagActivity.class.getName();
+
+    NfcAdapter mAdapter;
+    PendingIntent mPendingIntent;
+    TextView mTitle;
+    TextView mStatus;
+    TextView mCountView;
+    NdefMessage mMessage;
+    int mSize;
+    int mCount = 1;
+
+    final class MessageLoaderTask extends AsyncTask<String, Void, Cursor> {
+        @Override
+        public Cursor doInBackground(String... args) {
+            Cursor cursor = getContentResolver().query(
+                    NdefMessages.CONTENT_URI,
+                    new String[] { NdefMessages.TITLE, NdefMessages.BYTES },
+                    NdefMessages._ID + "=?",
+                    new String[] { args[0] }, null);
+
+            // Ensure the cursor executes and fills its window
+            if (cursor != null) cursor.getCount();
+            return cursor;
+        }
+
+        @Override
+        protected void onPostExecute(Cursor cursor) {
+            try {
+                if (cursor == null || !cursor.moveToFirst()) {
+                    setStatus("Failed to load tag for writing.", false);
+                    return;
+                }
+                byte[] blob = cursor.getBlob(1);
+                mSize = blob.length;
+                mMessage = new NdefMessage(blob);
+                mTitle.setText("Scan a tag to write\n" + cursor.getString(0));
+            } catch (FormatException e) {
+                setStatus("Invalid tag.", false);
+            } finally {
+                if (cursor != null) cursor.close();
+            }
+        }
+    }
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+
+        setContentView(R.layout.write_tag);
+        mTitle = (TextView) findViewById(R.id.title);
+        mStatus = (TextView) findViewById(R.id.status);
+        mCountView = (TextView) findViewById(R.id.count);
+
+        Bundle extras = getIntent().getExtras();
+        if (extras == null || !extras.containsKey("id")) {
+            setStatus("Nothing to write.", true);
+            return;
+        }
+
+        mTitle.setText("Loading tag.");
+        long id = extras.getLong("id");
+        new MessageLoaderTask().execute(Long.toString(id));
+
+        mAdapter = NfcAdapter.getDefaultAdapter(this);
+
+        // Create a generic PendingIntent that will be deliver to this activity. The NFC stack
+        // will fill in the intent with the details of the discovered tag before delivering to
+        // this activity.
+        mPendingIntent = PendingIntent.getActivity(this, 0,
+                new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        mAdapter.enableForegroundDispatch(this, mPendingIntent, null, null);
+    }
+
+    @Override
+    public void onNewIntent(Intent intent) {
+        Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
+        mCountView.setText("Tag " + mCount++);
+        if (mMessage != null) {
+            writeTag(tag);
+        } else {
+            setStatus("Not ready to write.", false);
+        }
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mAdapter.disableForegroundDispatch(this);
+    }
+
+    void setStatus(String message, boolean success) {
+        mStatus.setText(message);
+        if (!success) {
+            mStatus.setTextColor(Color.RED);
+        } else {
+            mStatus.setTextColor(Color.GREEN);
+        }
+    }
+
+    boolean writeTag(Tag tag) {
+        try {
+            Ndef ndef = Ndef.get(tag);
+            if (ndef != null) {
+                ndef.connect();
+
+                if (!ndef.isWritable()) {
+                    setStatus("Tag is read-only.", false);
+                    return false;
+                }
+                if (ndef.getMaxSize() < mSize) {
+                    setStatus("Tag capacity is " + ndef.getMaxSize() + " bytes, message is " +
+                            mSize + " bytes.", false);
+                    return false;
+                }
+
+                ndef.writeNdefMessage(mMessage);
+                setStatus("Wrote message to pre-formatted tag.", true);
+                return true;
+            } else {
+                NdefFormatable format = NdefFormatable.get(tag);
+                if (format != null) {
+                    try {
+                        format.connect();
+                        format.format(mMessage);
+                        setStatus("Formatted tag and wrote message.", true);
+                        return true;
+                    } catch (IOException e) {
+                        setStatus("Failed to format tag.", false);
+                        return false;
+                    }
+                } else {
+                    setStatus("Tag doesn't support NDEF.", false);
+                    return false;
+                }
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to write tag", e);
+        }
+
+        setStatus("Failed to write tag", false);
+        return false;
+    }
+}
index 111600e..3cc4523 100644 (file)
@@ -29,7 +29,7 @@ import android.database.sqlite.SQLiteOpenHelper;
 public class TagDBHelper extends SQLiteOpenHelper {
 
     private static final String DATABASE_NAME = "tags.db";
-    private static final int DATABASE_VERSION = 14;
+    private static final int DATABASE_VERSION = 16;
 
     public static final String TABLE_NAME_NDEF_MESSAGES = "ndef_msgs";
 
@@ -55,10 +55,30 @@ public class TagDBHelper extends SQLiteOpenHelper {
                 ");");
     }
 
-    @Override
-    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-        // Drop everything and recreate it for now
+    /**
+     * Drop data and recreate everything.
+     */
+    private void recreate(SQLiteDatabase db) {
         db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME_NDEF_MESSAGES);
         onCreate(db);
     }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        if (oldVersion < 14) {
+            // Pre-release version.
+            recreate(db);
+            db.setVersion(newVersion);
+        } else if (oldVersion == 14) {
+            // GB release - does not have My tags yet.
+            db.execSQL("ALTER TABLE " + TABLE_NAME_NDEF_MESSAGES + " ADD COLUMN "
+                    + NdefMessages.IS_MY_TAG + " INTEGER NOT NULL DEFAULT 0");
+            db.setVersion(newVersion);
+        } else if (oldVersion < DATABASE_VERSION) {
+            // Unreleased version with improperly formatted tags.
+            db.execSQL("DELETE FROM " + TABLE_NAME_NDEF_MESSAGES + " WHERE "
+                    + NdefMessages.IS_MY_TAG + "=1");
+            db.setVersion(newVersion);
+        }
+    }
 }
index 3b85825..1fd9153 100644 (file)
@@ -76,7 +76,6 @@ public class TagProvider extends SQLiteContentProvider implements TagProviderPip
         MATCHER.addURI(auth, "ndef_msgs", NDEF_MESSAGES);
         MATCHER.addURI(auth, "ndef_msgs/#", NDEF_MESSAGES_ID);
         MATCHER.addURI(auth, "ndef_msgs/#/#/mime", NDEF_MESSAGES_ID_MIME);
-
     }
 
     @Override
index ebaaf57..942b5f2 100644 (file)
@@ -20,28 +20,15 @@ import com.android.apps.tag.R;
 import com.google.common.base.Preconditions;
 
 import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
-import android.media.ThumbnailUtils;
 import android.nfc.NdefRecord;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.provider.MediaStore;
-import android.provider.OpenableColumns;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
-import android.widget.TextView;
-import android.widget.Toast;
 
 import java.io.ByteArrayOutputStream;
-import java.util.List;
 
 /**
  * A NdefRecord corresponding to an image type.
@@ -63,41 +50,6 @@ public class ImageRecord extends ParsedNdefRecord {
         return image;
     }
 
-    @Override
-    public RecordEditInfo getEditInfo(Activity host) {
-        return new ImageRecordEditInfo(mBitmap);
-    }
-
-    private static Intent getPickImageIntent() {
-        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
-        intent.addCategory(Intent.CATEGORY_OPENABLE);
-        intent.setType("image/*");
-        return intent;
-    }
-
-    /**
-     * Returns a view in a list of record types for adding new records to a message.
-     */
-    public static View getAddView(Context context, LayoutInflater inflater, ViewGroup parent) {
-        ViewGroup root = (ViewGroup) inflater.inflate(
-                R.layout.tag_add_record_list_item, parent, false);
-
-        // Determine which Activity can retrieve images.
-        Intent intent = getPickImageIntent();
-        PackageManager pm = context.getPackageManager();
-        List<ResolveInfo> activities = pm.queryIntentActivities(intent, 0);
-        if (activities.isEmpty()) {
-            return null;
-        }
-
-        ResolveInfo info = activities.get(0);
-        ((ImageView) root.findViewById(R.id.image)).setImageDrawable(info.loadIcon(pm));
-        ((TextView) root.findViewById(R.id.text)).setText(context.getString(R.string.photo));
-
-        root.setTag(new ImageRecordEditInfo());
-        return root;
-    }
-
     public static ImageRecord parse(NdefRecord record) {
         MimeRecord underlyingRecord = MimeRecord.parse(record);
         Preconditions.checkArgument(underlyingRecord.getMimeType().startsWith("image/"));
@@ -126,149 +78,4 @@ public class ImageRecord extends ParsedNdefRecord {
         byte[] content = out.toByteArray();
         return MimeRecord.newMimeRecord("image/jpeg", content);
     }
-
-    private static class ImageRecordEditInfo extends RecordEditInfo {
-        /**
-         * The path on the device where we can load the image. If this is set, the value will be
-         * lazily loaded from the path.
-         */
-        private String mCurrentPath;
-
-        /**
-         * The actual current value of the image for the record.
-         */
-        private Bitmap mCachedValue;
-
-        /**
-         * Pixel size to crop loaded images to.
-         */
-        public static final int MAX_IMAGE_SIZE = 128;
-
-        public ImageRecordEditInfo() {
-            super(RECORD_TYPE);
-            mCurrentPath = "";
-            mCachedValue = null;
-        }
-
-        public ImageRecordEditInfo(String path) {
-            super(RECORD_TYPE);
-            mCurrentPath = path;
-            mCachedValue = null;
-        }
-
-        public ImageRecordEditInfo(Bitmap value) {
-            super(RECORD_TYPE);
-            mCurrentPath = "";
-            mCachedValue = value;
-        }
-
-        protected ImageRecordEditInfo(Parcel parcel) {
-            super(parcel);
-            mCurrentPath = parcel.readString();
-            mCachedValue = parcel.readParcelable(null);
-        }
-
-        @Override
-        public Intent getPickIntent() {
-            return getPickImageIntent();
-        }
-
-        @Override
-        public NdefRecord getValue() {
-            return ImageRecord.newImageRecord(getValueInternal());
-        }
-
-        private Bitmap getValueInternal() {
-            if (mCachedValue == null) {
-                Bitmap original = BitmapFactory.decodeFile(mCurrentPath);
-                int width = original.getWidth();
-                int height = original.getHeight();
-                int major = (width > height) ? width : height;
-                if (major > MAX_IMAGE_SIZE) {
-                    double scale = 1.0 * MAX_IMAGE_SIZE / major;
-                    width *= scale;
-                    height *= scale;
-                }
-                mCachedValue = ThumbnailUtils.extractThumbnail(original, width, height);
-            }
-            return mCachedValue;
-        }
-
-        @Override
-        public void handlePickResult(Context context, Intent data) {
-            Cursor cursor = null;
-            mCachedValue = null;
-
-            try {
-                String[] projection = { MediaStore.Images.Media.DATA, OpenableColumns.SIZE };
-                cursor = context.getContentResolver().query(
-                        data.getData(), projection, null, null, null);
-
-                if (cursor == null) {
-                    Toast.makeText(
-                            context,
-                            context.getResources().getString(R.string.bad_photo),
-                            Toast.LENGTH_LONG).show();
-                    throw new IllegalArgumentException("Selected image could not be loaded");
-                }
-
-                cursor.moveToFirst();
-                int size = cursor.getInt(1);
-                mCurrentPath = cursor.getString(0);
-
-                // TODO: enforce a size limit. May be tricky.
-
-            } finally {
-                if (cursor != null) {
-                    cursor.close();
-                }
-            }
-        }
-
-        @Override
-        public View getEditView(
-                Activity activity, LayoutInflater inflater,
-                ViewGroup parent, EditCallbacks callbacks) {
-            View result = buildEditView(
-                    activity, inflater, R.layout.tag_edit_image, parent, callbacks);
-            ((ImageView) result.findViewById(R.id.image)).setImageBitmap(getValueInternal());
-            result.setOnClickListener(this);
-            return result;
-        }
-
-        @Override
-        public void writeToParcel(Parcel out, int flags) {
-            super.writeToParcel(out, flags);
-            out.writeString(mCurrentPath);
-            out.writeParcelable(mCachedValue, flags);
-        }
-
-        @SuppressWarnings("unused")
-        public static final Parcelable.Creator<ImageRecordEditInfo> CREATOR =
-                new Parcelable.Creator<ImageRecordEditInfo>() {
-            @Override
-            public ImageRecordEditInfo createFromParcel(Parcel in) {
-                return new ImageRecordEditInfo(in);
-            }
-
-            @Override
-            public ImageRecordEditInfo[] newArray(int size) {
-                return new ImageRecordEditInfo[size];
-            }
-        };
-
-        @Override
-        public int describeContents() {
-            return 0;
-        }
-
-        @Override
-        public void onClick(View target) {
-            if (this == target.getTag()) {
-                mCallbacks.startPickForRecord(this, getPickIntent());
-            } else {
-                super.onClick(target);
-            }
-        }
-    }
 }
index 6b0bf6e..a5ecbbe 100644 (file)
@@ -93,9 +93,7 @@ public abstract class RecordEditInfo implements Parcelable, View.OnClickListener
         result.setTag(this);
 
         View deleteButton = result.findViewById(R.id.delete);
-        if (deleteButton == null) {
-            throw new IllegalArgumentException("All record edit layouts must have a delete button");
-        } else {
+        if (deleteButton != null) {
             deleteButton.setOnClickListener(this);
         }
         mCallbacks = callbacks;
index 3f7e175..e598937 100644 (file)
@@ -292,7 +292,8 @@ public class UriRecord extends ParsedNdefRecord implements OnClickListener {
         }
 
         protected UriRecordEditInfo(Parcel parcel) {
-            this(parcel.readString());
+            super(parcel);
+            mCurrentValue = parcel.readString();
         }
 
         @Override
@@ -302,6 +303,7 @@ public class UriRecord extends ParsedNdefRecord implements OnClickListener {
 
         @Override
         public void handlePickResult(Context context, Intent data) {
+            // Not supported.
         }
 
         @Override
index 362a447..32641a9 100644 (file)
@@ -144,8 +144,8 @@ public class VCardRecord extends ParsedNdefRecord implements OnClickListener {
     }
 
     private static Intent getPickContactIntent() {
-        Intent intent = new Intent(Intent.ACTION_PICK);
-        intent.setType(ContactsContract.Contacts.CONTENT_TYPE);
+        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+        intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
         return intent;
     }
 
@@ -264,7 +264,7 @@ public class VCardRecord extends ParsedNdefRecord implements OnClickListener {
         }
 
         protected VCardRecordEditInfo(Parcel parcel) {
-            super(RECORD_TYPE);
+            super(parcel);
             mLookupUri = parcel.readParcelable(null);
             int valueLength = parcel.readInt();
             if (valueLength > 0) {
index cc7df4f..4ea20c6 100644 (file)
@@ -1,7 +1,7 @@
 LOCAL_PATH:= $(call my-dir)
 include $(CLEAR_VARS)
 
-#LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
 
 LOCAL_JAVA_LIBRARIES := android.test.runner
 
@@ -11,4 +11,4 @@ LOCAL_MODULE_TAGS := tests
 
 LOCAL_INSTRUMENTATION_FOR := Tag
 
-#include $(BUILD_PACKAGE)
+include $(BUILD_PACKAGE)