Usability fixes for tags and DB upgrade code.
- 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
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index afeded9..b38ce66 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -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
index 0000000..cdc05a8
--- /dev/null
+++ b/res/drawable-hdpi/active_tag_icon.png
Binary files differ
diff --git a/res/drawable-mdpi/active_tag_icon.png b/res/drawable-mdpi/active_tag_icon.png
new file mode 100644
index 0000000..30ead23
--- /dev/null
+++ b/res/drawable-mdpi/active_tag_icon.png
Binary files differ
diff --git a/res/layout/tag_list_item.xml b/res/layout/tag_list_item.xml
index 75fa953..4f5f559 100644
--- a/res/layout/tag_list_item.xml
+++ b/res/layout/tag_list_item.xml
@@ -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/values/strings.xml b/res/values/strings.xml
index 47fd1e8..5ef6a53 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -195,4 +195,12 @@
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/src/com/android/apps/tag/EditTagActivity.java b/src/com/android/apps/tag/EditTagActivity.java
index 3694db3..c35b078 100644
--- a/src/com/android/apps/tag/EditTagActivity.java
+++ b/src/com/android/apps/tag/EditTagActivity.java
@@ -304,9 +304,8 @@
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();
diff --git a/src/com/android/apps/tag/MyTagList.java b/src/com/android/apps/tag/MyTagList.java
index a4759f2..06d4db7 100644
--- a/src/com/android/apps/tag/MyTagList.java
+++ b/src/com/android/apps/tag/MyTagList.java
@@ -18,7 +18,6 @@
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.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.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 @@
*/
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 @@
private TagAdapter mAdapter;
private long mActiveTagId;
+ private Uri mTagBeingSaved;
private NdefMessage mActiveTag;
private WeakReference<SelectActiveTagDialog> mSelectActiveTagDialog;
@@ -143,6 +147,12 @@
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 @@
super.onDestroy();
}
-
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
editTag(id);
@@ -225,19 +234,14 @@
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 @@
static final class ViewHolder {
public CharArrayBuffer titleBuffer;
public TextView mainLine;
+ public ImageView activeIcon;
}
/**
@@ -279,6 +284,9 @@
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 @@
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 @@
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 @@
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 @@
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 @@
* 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 @@
.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 @@
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 @@
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
selectActiveTag(position);
- enableSharing();
+ enableSharingAndStoreTag();
cancel();
}
}
-
}
diff --git a/src/com/android/apps/tag/TagService.java b/src/com/android/apps/tag/TagService.java
index 5847bc2..bca13b6 100644
--- a/src/com/android/apps/tag/TagService.java
+++ b/src/com/android/apps/tag/TagService.java
@@ -21,11 +21,15 @@
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 @@
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 @@
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 });
diff --git a/src/com/android/apps/tag/provider/TagDBHelper.java b/src/com/android/apps/tag/provider/TagDBHelper.java
index 650dba7..3cc4523 100644
--- a/src/com/android/apps/tag/provider/TagDBHelper.java
+++ b/src/com/android/apps/tag/provider/TagDBHelper.java
@@ -55,10 +55,30 @@
");");
}
- @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);
+ }
+ }
}