Usability fixes for tags and DB upgrade code.
Ben Komalo [Tue, 18 Jan 2011 22:45:37 +0000 (14:45 -0800)]
- made creation of a new tag auto-select it as the active one
- add icon to the selected tag
- make tapping on "select active tag" less confusing when
  there are no tags in your list

Bug: 3351616
Bug: 3350480
Bug: 3349221
Bug: 3349209
Change-Id: I683151bf6aae9059a68b3c8f3516592ed1d42777

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/tag_list_item.xml
res/values/strings.xml
src/com/android/apps/tag/EditTagActivity.java
src/com/android/apps/tag/MyTagList.java
src/com/android/apps/tag/TagService.java
src/com/android/apps/tag/provider/TagDBHelper.java

index afeded9..b38ce66 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" />
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 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>
 
index 47fd1e8..5ef6a53 100644 (file)
@@ -195,4 +195,12 @@ Networks).</string>
          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>
index 3694db3..c35b078 100644 (file)
@@ -304,9 +304,8 @@ public class EditTagActivity extends Activity implements OnClickListener, EditCa
         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.
-            TagService.saveMyMessages(this, new NdefMessage[] { msg });
-
             Intent openMyTags = new Intent(this, MyTagList.class);
+            openMyTags.putExtra(EXTRA_RESULT_MSG, msg);
             startActivity(openMyTags);
             finish();
 
index a4759f2..06d4db7 100644 (file)
@@ -18,7 +18,6 @@ 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.ImageRecord;
 import com.android.apps.tag.record.RecordEditInfo;
 import com.android.apps.tag.record.UriRecord;
 import com.android.apps.tag.record.VCardRecord;
@@ -32,10 +31,12 @@ 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;
@@ -55,6 +56,7 @@ 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;
@@ -71,7 +73,8 @@ import java.util.Set;
  */
 public class MyTagList
         extends Activity
-        implements OnItemClickListener, View.OnClickListener, SelectContentCallbacks {
+        implements OnItemClickListener, View.OnClickListener,
+                   SelectContentCallbacks, TagService.SaveCallbacks {
 
     static final String TAG = "TagList";
 
@@ -91,6 +94,7 @@ public class MyTagList
 
     private TagAdapter mAdapter;
     private long mActiveTagId;
+    private Uri mTagBeingSaved;
     private NdefMessage mActiveTag;
 
     private WeakReference<SelectActiveTagDialog> mSelectActiveTagDialog;
@@ -143,6 +147,12 @@ public class MyTagList
             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
@@ -165,7 +175,6 @@ public class MyTagList
         super.onDestroy();
     }
 
-
     @Override
     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
         editTag(id);
@@ -225,19 +234,14 @@ public class MyTagList
                 setEmptyView();
             } else {
                 // Find the active tag.
-                if (mActiveTagId != -1) {
+                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());
-
-                            // 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(MyTagList.this)
-                                    .getLocalNdefMessage() != null) {
-                                enableSharing();
-                            }
                             break;
                         }
                     }
@@ -259,6 +263,7 @@ public class MyTagList
     static final class ViewHolder {
         public CharArrayBuffer titleBuffer;
         public TextView mainLine;
+        public ImageView activeIcon;
     }
 
     /**
@@ -279,6 +284,9 @@ public class MyTagList
             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
@@ -289,6 +297,7 @@ public class MyTagList
             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);
 
@@ -309,13 +318,12 @@ public class MyTagList
                 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();
                 }
 
@@ -327,6 +335,27 @@ public class MyTagList
                 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;
         }
@@ -420,11 +449,25 @@ public class MyTagList
             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) {
@@ -441,8 +484,8 @@ public class MyTagList
      * 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();
@@ -458,8 +501,15 @@ public class MyTagList
                         .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.
@@ -469,12 +519,37 @@ public class MyTagList
             updateActiveTagView(null);
             disableSharing();
         }
+        mTagBeingSaved = null;
     }
 
-    private void enableSharing() {
+    /**
+     * 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 enableSharingAndStoreTag() {
         mEnabled.setChecked(true);
         NfcAdapter.getDefaultAdapter(this).setLocalNdefMessage(
-            Preconditions.checkNotNull(mActiveTag));
+                Preconditions.checkNotNull(mActiveTag));
     }
 
     private void disableSharing() {
@@ -560,9 +635,8 @@ public class MyTagList
         @Override
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
             selectActiveTag(position);
-            enableSharing();
+            enableSharingAndStoreTag();
             cancel();
         }
     }
-
 }
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 650dba7..3cc4523 100644 (file)
@@ -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);
+        }
+    }
 }