A bunch of work on the UI.

- Change the tab order and remember the
  current tab
- Proper icon for the starred tab, renamed
  from saved
- Set the proper sort order for the tag lists
- Very rough first pass at full screen tag viewer
- Hookup the delete button in the tag viewer
- Store the snippets for tags in the database
- Added view creation logic to the parsed
  messages and records so they can render
  themselves
- Make the URI records look much better
- For URI records if there are multiple activities
  that can handle the URI build one item per
  activity and bypass the activity chooser
- Pretty print sms[to] and tel URIs
- Hookup URI entries in the viewer to launch the
  activities
- Implement the spec for saving tags and timing
  out the viewer for scanned tags
- Made a few more strings localizable

Change-Id: I6bdb8adf52445499c62a1b046f99d5b119aff068
This commit is contained in:
Jeff Hamilton
2010-10-15 09:42:07 -05:00
parent b19cc7baa9
commit f8580cf676
30 changed files with 688 additions and 150 deletions

View File

@@ -23,6 +23,7 @@
package="com.android.apps.tag"
>
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.NFC" />
<application android:label="Tags">
@@ -39,7 +40,7 @@
<activity android:name="TagList" />
<activity android:name="TagViewer"
android:theme="@android:style/Theme.Dialog"
android:theme="@android:style/Theme.NoTitleBar"
>
<intent-filter>
<action android:name="android.nfc.action.NDEF_TAG_DISCOVERED"/>
@@ -47,5 +48,7 @@
</intent-filter>
</activity>
<service android:name="TagService" />
</application>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

View File

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

View File

@@ -33,7 +33,6 @@
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="5dp" />
android:layout_height="match_parent" />
</LinearLayout>
</TabHost>

View File

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

View File

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

View File

@@ -14,16 +14,29 @@
limitations under the License.
-->
<resources>
<string name="hello_activity_text_text">Hello, World!</string>
<string name="help_and_info">help and info</string>
<string name="saved">Saved</string>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- The title of the tab that displays all recently scanned NFC tags -->
<string name="tab_recent">Recent</string>
<string name="tab_tags">Tags</string>
<!-- The title of the tab that displays all saved NFC tags -->
<string name="tab_saved">Saved</string>
<!-- The title of the tab that displays all starred NFC tags -->
<string name="tab_starred">Starred</string>
<!-- The title displayed for unknown tag types -->
<string name="tag_unknown">Unknown tag type</string>
<!-- The title displayed for an empty tag -->
<string name="tag_empty">Empty tag</string>
<!-- Button label indicating that the user wants to delete a tag -->
<string name="button_delete">Delete</string>
<!-- String describing that if the user doesn't want to save a tag they should touch the button labeled Cancel. The text for the cancel button comes from the system string android.R.string.cancel. -->
<string name="cancel_help_text">To skip adding this tag to your collection, press Cancel</string>
<!-- String displayed for an action to send a text message to a phone number -->
<string name="action_text">Text <xliff:g id="phone_number">%s</xliff:g></string>
<!-- String displayed for an action to call a phone number -->
<string name="action_call">Call <xliff:g id="phone_number">%s</xliff:g></string>
</resources>

View File

@@ -14,9 +14,12 @@
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/help_info_menu_item"
android:icon="@android:drawable/ic_menu_help"
android:title="@string/help_and_info" />
</menu>
<resources>
<style name="ButtonBar">
<item name="android:paddingTop">5dip</item>
<item name="android:paddingLeft">4dip</item>
<item name="android:paddingRight">4dip</item>
<item name="android:paddingBottom">1dip</item>
<item name="android:background">@android:color/black</item>
</style>
</resources>

View File

@@ -16,12 +16,11 @@
package com.android.apps.tag;
import com.android.apps.tag.TagDBHelper.NdefMessagesTable;
import android.content.Context;
import android.database.Cursor;
import android.nfc.FormatException;
import android.nfc.NdefMessage;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -29,12 +28,6 @@ import android.widget.Adapter;
import android.widget.CursorAdapter;
import android.widget.TextView;
import com.android.apps.tag.TagDBHelper.NdefMessagesTable;
import com.android.apps.tag.message.NdefMessageParser;
import com.android.apps.tag.message.ParsedNdefMessage;
import java.util.Locale;
/**
* A custom {@link Adapter} that renders tag entries for a list.
*/
@@ -52,19 +45,7 @@ public class TagAdapter extends CursorAdapter {
TextView mainLine = (TextView) view.findViewById(R.id.title);
TextView dateLine = (TextView) view.findViewById(R.id.date);
NdefMessage msg = null;
try {
msg = new NdefMessage(cursor.getBlob(cursor.getColumnIndex(NdefMessagesTable.BYTES)));
} catch (FormatException e) {
Log.e("foo", "poorly formatted message", e);
}
if (msg == null) {
mainLine.setText("Invalid tag");
} else {
ParsedNdefMessage parsedMsg = NdefMessageParser.parse(msg);
mainLine.setText(parsedMsg.getSnippet(Locale.getDefault()));
}
mainLine.setText(cursor.getString(cursor.getColumnIndex(NdefMessagesTable.TITLE)));
dateLine.setText(DateUtils.getRelativeTimeSpanString(
context, cursor.getLong(cursor.getColumnIndex(NdefMessagesTable.DATE))));
}

View File

@@ -18,7 +18,9 @@ package com.android.apps.tag;
import android.app.Activity;
import android.app.TabActivity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Bundle;
import android.widget.TabHost;
@@ -36,16 +38,34 @@ public class TagBrowserActivity extends TabActivity {
Resources res = getResources();
TabHost tabHost = getTabHost();
TabHost.TabSpec spec1 = tabHost.newTabSpec("saved")
.setIndicator(getText(R.string.tab_saved), res.getDrawable(R.drawable.ic_menu_tag))
.setContent(new Intent().setClass(this, TagList.class)
.putExtra(TagList.EXTRA_SHOW_SAVED_ONLY, true));
tabHost.addTab(spec1);
tabHost.addTab(tabHost.newTabSpec("tags")
.setIndicator(getText(R.string.tab_tags),
res.getDrawable(R.drawable.ic_menu_tag))
.setContent(new Intent().setClass(this, TagList.class)));
TabHost.TabSpec spec2 = tabHost.newTabSpec("recent")
.setIndicator(getText(R.string.tab_recent), res.getDrawable(R.drawable.ic_menu_desk_clock))
.setContent(new Intent().setClass(this, TagList.class));
tabHost.addTab(spec2);
tabHost.addTab(tabHost.newTabSpec("starred")
.setIndicator(getText(R.string.tab_starred),
res.getDrawable(R.drawable.ic_tab_starred))
.setContent(new Intent().setClass(this, TagList.class)
.putExtra(TagList.EXTRA_SHOW_STARRED_ONLY, true)));
}
@Override
public void onStart() {
super.onStart();
// Restore the last active tab
SharedPreferences prefs = getSharedPreferences("prefs", Context.MODE_PRIVATE);
getTabHost().setCurrentTabByTag(prefs.getString("tab", "tags"));
}
@Override
public void onStop() {
super.onStop();
// Save the active tab
SharedPreferences.Editor edit = getSharedPreferences("prefs", Context.MODE_PRIVATE).edit();
edit.putString("tab", getTabHost().getCurrentTabTag());
edit.apply();
}
}

View File

@@ -25,6 +25,10 @@ import android.nfc.FormatException;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import java.util.Locale;
import com.android.apps.tag.message.NdefMessageParser;
import com.android.apps.tag.message.ParsedNdefMessage;
import com.google.common.annotations.VisibleForTesting;
/**
@@ -33,7 +37,7 @@ import com.google.common.annotations.VisibleForTesting;
public class TagDBHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "tags.db";
private static final int DATABASE_VERSION = 3;
private static final int DATABASE_VERSION = 5;
public interface NdefMessagesTable {
public static final String TABLE_NAME = "nedf_msg";
@@ -42,11 +46,13 @@ public class TagDBHelper extends SQLiteOpenHelper {
public static final String TITLE = "title";
public static final String BYTES = "bytes";
public static final String DATE = "date";
public static final String SAVED = "saved";
public static final String STARRED = "starred";
}
private static TagDBHelper sInstance;
private Context mContext;
public static synchronized TagDBHelper getInstance(Context context) {
if (sInstance == null) {
sInstance = new TagDBHelper(context.getApplicationContext());
@@ -56,11 +62,13 @@ public class TagDBHelper extends SQLiteOpenHelper {
private TagDBHelper(Context context) {
this(context, DATABASE_NAME);
mContext = context;
}
@VisibleForTesting
TagDBHelper(Context context, String dbFile) {
super(context, dbFile, null, DATABASE_VERSION);
mContext = context;
}
@Override
@@ -70,12 +78,12 @@ public class TagDBHelper extends SQLiteOpenHelper {
NdefMessagesTable.TITLE + " TEXT NOT NULL DEFAULT ''," +
NdefMessagesTable.BYTES + " BLOB NOT NULL, " +
NdefMessagesTable.DATE + " INTEGER NOT NULL, " +
NdefMessagesTable.SAVED + " INTEGER NOT NULL DEFAULT 0" + // boolean
NdefMessagesTable.STARRED + " INTEGER NOT NULL DEFAULT 0" + // boolean
");");
db.execSQL("CREATE INDEX msgIndex ON " + NdefMessagesTable.TABLE_NAME + " (" +
NdefMessagesTable.DATE + " DESC, " +
NdefMessagesTable.SAVED + " ASC" +
NdefMessagesTable.STARRED + " ASC" +
")");
addTestData(db);
@@ -114,15 +122,18 @@ public class TagDBHelper extends SQLiteOpenHelper {
}
}
public void insertNdefMessage(SQLiteDatabase db, NdefMessage msg, boolean isSaved) {
public void insertNdefMessage(SQLiteDatabase db, NdefMessage msg, boolean isStarred) {
ParsedNdefMessage parsedMsg = NdefMessageParser.parse(msg);
SQLiteStatement stmt = null;
try {
stmt = db.compileStatement("INSERT INTO " + NdefMessagesTable.TABLE_NAME +
"(" + NdefMessagesTable.BYTES + ", " + NdefMessagesTable.DATE + ", " +
NdefMessagesTable.SAVED + ") values (?, ?, ?)");
NdefMessagesTable.STARRED + "," + NdefMessagesTable.TITLE + ") " +
"values (?, ?, ?, ?)");
stmt.bindBlob(1, msg.toByteArray());
stmt.bindLong(2, System.currentTimeMillis());
stmt.bindLong(3, isSaved ? 1 : 0);
stmt.bindLong(3, isStarred ? 1 : 0);
stmt.bindString(4, parsedMsg.getSnippet(mContext, Locale.getDefault()));
stmt.executeInsert();
} finally {
if (stmt != null) stmt.close();

View File

@@ -41,7 +41,7 @@ import com.android.apps.tag.TagDBHelper.NdefMessagesTable;
public class TagList extends ListActivity implements DialogInterface.OnClickListener {
static final String TAG = "TagList";
static final String EXTRA_SHOW_SAVED_ONLY = "show_saved_only";
static final String EXTRA_SHOW_STARRED_ONLY = "show_starred_only";
SQLiteDatabase mDatabase;
TagAdapter mAdapter;
@@ -50,9 +50,9 @@ public class TagList extends ListActivity implements DialogInterface.OnClickList
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
boolean showSavedOnly = getIntent().getBooleanExtra(EXTRA_SHOW_SAVED_ONLY, false);
boolean showStarredOnly = getIntent().getBooleanExtra(EXTRA_SHOW_STARRED_ONLY, false);
mDatabase = TagDBHelper.getInstance(this).getReadableDatabase();
String selection = showSavedOnly ? NdefMessagesTable.SAVED + "=1" : null;
String selection = showStarredOnly ? NdefMessagesTable.STARRED + "=1" : null;
new TagLoaderTask().execute(selection);
mAdapter = new TagAdapter(this);
@@ -119,7 +119,7 @@ public class TagList extends ListActivity implements DialogInterface.OnClickList
NdefMessagesTable.DATE,
NdefMessagesTable.TITLE },
selection,
null, null, null, null);
null, null, null, NdefMessagesTable.DATE + " DESC");
cursor.getCount();
return cursor;
}

View File

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

View File

@@ -16,39 +16,57 @@
package com.android.apps.tag;
import com.android.apps.tag.message.NdefMessageParser;
import com.android.apps.tag.message.ParsedNdefMessage;
import com.android.apps.tag.record.ParsedNdefRecord;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Color;
import android.nfc.NdefMessage;
import android.nfc.NdefTag;
import android.nfc.NfcAdapter;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.apps.tag.message.NdefMessageParser;
import com.android.apps.tag.message.ParsedNdefMessage;
import java.util.Locale;
/**
* An {@link Activity} which handles a broadcast of a new tag that the device just discovered.
*/
public class TagViewer extends Activity {
public class TagViewer extends Activity implements OnClickListener, Handler.Callback {
static final String TAG = "SaveTag";
static final String EXTRA_TAG_DB_ID = "db_id";
static final String EXTRA_MESSAGE = "msg";
/** This activity will finish itself in this amount of time if the user doesn't do anything. */
static final int ACTIVITY_TIMEOUT_MS = 10 * 1000;
long mTagDatabaseId;
ImageView mIcon;
TextView mTitle;
CheckBox mStar;
Button mDeleteButton;
Button mCancelButton;
NdefMessage[] mMessagesToSave = null;
@Override
protected void onStart() {
super.onStart();
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
@@ -58,6 +76,18 @@ public class TagViewer extends Activity {
| WindowManager.LayoutParams.FLAG_DIM_BEHIND
);
setContentView(R.layout.tag_viewer);
mTitle = (TextView) findViewById(R.id.title);
mIcon = (ImageView) findViewById(R.id.icon);
mStar = (CheckBox) findViewById(R.id.star);
mDeleteButton = (Button) findViewById(R.id.btn_delete);
mCancelButton = (Button) findViewById(R.id.btn_cancel);
mDeleteButton.setOnClickListener(this);
mCancelButton.setOnClickListener(this);
mIcon.setImageResource(R.drawable.ic_launcher_nfc);
Intent intent = getIntent();
NdefMessage[] msgs = null;
NdefTag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
@@ -68,10 +98,18 @@ public class TagViewer extends Activity {
if (msg != null) {
msgs = new NdefMessage[] { msg };
}
// Hide the text about saving the tag, it's already in the database
findViewById(R.id.cancel_help_text).setVisibility(View.GONE);
} else {
msgs = tag.getNdefMessages();
// TODO use a service to avoid the process getting reaped during saving
new SaveTagTask().execute(msgs);
mDeleteButton.setVisibility(View.GONE);
// Set a timer on this activity since it wasn't created by the user
new Handler(this).sendEmptyMessageDelayed(0, ACTIVITY_TIMEOUT_MS);
// Save the messages that were just scanned
mMessagesToSave = msgs;
}
if (msgs == null || msgs.length == 0) {
@@ -80,43 +118,72 @@ public class TagViewer extends Activity {
return;
}
LayoutInflater inflater = LayoutInflater.from(
new ContextThemeWrapper(this, android.R.style.Theme_Light));
LinearLayout list = (LinearLayout) inflater.inflate(R.layout.tag_viewer_list, null, false);
// TODO figure out why the background isn't white, the CTW should force that...
list.setBackgroundColor(Color.WHITE);
setContentView(list);
Context contentContext = new ContextThemeWrapper(this, android.R.style.Theme_Light);
LayoutInflater inflater = LayoutInflater.from(contentContext);
LinearLayout list = (LinearLayout) findViewById(R.id.list);
buildTagViews(list, inflater, msgs);
if (TextUtils.isEmpty(getTitle())) {
// There isn't a snippet for this tag, use a default title
setTitle(R.string.tag_unknown);
}
}
private void buildTagViews(LinearLayout list, LayoutInflater inflater, NdefMessage[] msgs) {
// The body of the dialog should use the light theme
if (msgs == null || msgs.length == 0) {
return;
}
// Build the views from the logical records in the messages
for (NdefMessage msg : msgs) {
ParsedNdefMessage parsedMsg = NdefMessageParser.parse(msg);
TextView text = (TextView) inflater.inflate(R.layout.tag_text, list, false);
text.setText(parsedMsg.getSnippet(Locale.getDefault()));
list.addView(text);
NdefMessage msg = msgs[0];
// Set the title to be the snippet of the message
ParsedNdefMessage parsedMsg = NdefMessageParser.parse(msg);
setTitle(parsedMsg.getSnippet(this, Locale.getDefault()));
// Build views for all of the sub records
for (ParsedNdefRecord record : parsedMsg.getRecords()) {
list.addView(record.getView(this, inflater, list));
inflater.inflate(R.layout.tag_divider, list, true);
}
}
final class SaveTagTask extends AsyncTask<NdefMessage, Void, Void> {
@Override
public Void doInBackground(NdefMessage... msgs) {
TagDBHelper helper = TagDBHelper.getInstance(TagViewer.this);
SQLiteDatabase db = helper.getWritableDatabase();
db.beginTransaction();
try {
for (NdefMessage msg : msgs) {
helper.insertNdefMessage(db, msg, false);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return null;
@Override
public void setTitle(CharSequence title) {
mTitle.setText(title);
}
@Override
public void onClick(View view) {
if (view == mDeleteButton) {
Intent save = new Intent(this, TagService.class);
save.putExtra(TagService.EXTRA_DELETE_ID, mTagDatabaseId);
startService(save);
finish();
} else if (view == mCancelButton) {
mMessagesToSave = null;
finish();
}
}
@Override
public void onStop() {
super.onStop();
if (mMessagesToSave != null) {
saveMessages(mMessagesToSave);
}
}
void saveMessages(NdefMessage[] msgs) {
Intent save = new Intent(this, TagService.class);
save.putExtra(TagService.EXTRA_SAVE_MSGS, msgs);
startService(save);
}
@Override
public boolean handleMessage(Message msg) {
finish();
return true;
}
}

View File

@@ -16,18 +16,26 @@
package com.android.apps.tag.message;
import com.android.apps.tag.R;
import com.android.apps.tag.record.ParsedNdefRecord;
import android.content.Context;
import java.util.ArrayList;
import java.util.Locale;
/**
* A parsed message containing no elements.
*/
class EmptyMessage implements ParsedNdefMessage {
class EmptyMessage extends ParsedNdefMessage {
/* package private */ EmptyMessage() { }
/* package private */ EmptyMessage() {
super(new ArrayList<ParsedNdefRecord>());
}
@Override
public String getSnippet(Locale locale) {
return "Empty Tag"; // TODO: localize
public String getSnippet(Context context, Locale locale) {
return context.getString(R.string.tag_empty);
}
@Override

View File

@@ -47,13 +47,13 @@ public class NdefMessageParser {
if (elements.size() == 1) {
if (first instanceof SmartPoster) {
return new SmartPosterMessage((SmartPoster) first);
return new SmartPosterMessage((SmartPoster) first, elements);
}
if (first instanceof TextRecord) {
return new TextMessage((TextRecord) first);
return new TextMessage((TextRecord) first, elements);
}
if (first instanceof UriRecord) {
return new UriMessage((UriRecord) first);
return new UriMessage((UriRecord) first, elements);
}
}

View File

@@ -16,20 +16,39 @@
package com.android.apps.tag.message;
import com.android.apps.tag.record.ParsedNdefRecord;
import com.google.common.collect.ImmutableList;
import android.content.Context;
import java.util.List;
import java.util.Locale;
/**
* A parsed version of an {@link android.nfc.NdefMessage}
*/
public interface ParsedNdefMessage {
public abstract class ParsedNdefMessage {
private List<ParsedNdefRecord> mRecords;
public ParsedNdefMessage(List<ParsedNdefRecord> records) {
mRecords = ImmutableList.copyOf(records);
}
/**
* Returns the list of parsed records on this message.
*/
public List<ParsedNdefRecord> getRecords() {
return mRecords;
}
/**
* Returns the snippet information associated with the NdefMessage
* most appropriate for the given {@code locale}.
*/
public String getSnippet(Locale locale);
public abstract String getSnippet(Context context, Locale locale);
// TODO: Determine if this is the best place for holding whether
// the user has starred this parsed message.
public boolean isStarred();
public abstract boolean isStarred();
}

View File

@@ -16,27 +16,32 @@
package com.android.apps.tag.message;
import com.android.apps.tag.record.ParsedNdefRecord;
import com.android.apps.tag.record.SmartPoster;
import com.android.apps.tag.record.TextRecord;
import com.google.common.base.Preconditions;
import android.content.Context;
import java.util.List;
import java.util.Locale;
/**
* A message consisting of one {@link SmartPoster} object.
*/
class SmartPosterMessage implements ParsedNdefMessage {
class SmartPosterMessage extends ParsedNdefMessage {
private final SmartPoster mPoster;
SmartPosterMessage(SmartPoster poster) {
SmartPosterMessage(SmartPoster poster, List<ParsedNdefRecord> records) {
super(Preconditions.checkNotNull(records));
mPoster = Preconditions.checkNotNull(poster);
}
@Override
public String getSnippet(Locale locale) {
public String getSnippet(Context context, Locale locale) {
TextRecord title = mPoster.getTitle();
if (title == null) {
return mPoster.getUriRecord().getUri().toString();
return mPoster.getUriRecord().getPrettyUriString(context);
}
return title.getText();
}

View File

@@ -16,23 +16,28 @@
package com.android.apps.tag.message;
import com.android.apps.tag.record.ParsedNdefRecord;
import com.android.apps.tag.record.TextRecord;
import com.google.common.base.Preconditions;
import android.content.Context;
import java.util.List;
import java.util.Locale;
/**
* A message containing one text element
*/
class TextMessage implements ParsedNdefMessage {
class TextMessage extends ParsedNdefMessage {
private final TextRecord mRecord;
TextMessage(TextRecord record) {
TextMessage(TextRecord record, List<ParsedNdefRecord> records) {
super(Preconditions.checkNotNull(records));
mRecord = Preconditions.checkNotNull(record);
}
@Override
public String getSnippet(Locale locale) {
public String getSnippet(Context context, Locale locale) {
return mRecord.getText();
}

View File

@@ -16,26 +16,27 @@
package com.android.apps.tag.message;
import com.android.apps.tag.R;
import com.android.apps.tag.record.ParsedNdefRecord;
import com.google.common.collect.ImmutableList;
import com.google.common.base.Preconditions;
import android.content.Context;
import java.util.List;
import java.util.Locale;
/**
* The catchall parsed message format for when nothing else better applies.
*/
class UnknownMessage implements ParsedNdefMessage {
class UnknownMessage extends ParsedNdefMessage {
private final ImmutableList<ParsedNdefRecord> mRecords;
UnknownMessage(Iterable<ParsedNdefRecord> records) {
mRecords = ImmutableList.copyOf(records);
UnknownMessage(List<ParsedNdefRecord> records) {
super(Preconditions.checkNotNull(records));
}
@Override
public String getSnippet(Locale locale) {
// TODO: localize
return "Unknown record type with " + mRecords.size() + " elements.";
public String getSnippet(Context context, Locale locale) {
return context.getString(R.string.tag_unknown);
}
@Override

View File

@@ -16,26 +16,31 @@
package com.android.apps.tag.message;
import com.android.apps.tag.record.ParsedNdefRecord;
import com.android.apps.tag.record.UriRecord;
import com.google.common.base.Preconditions;
import android.content.Context;
import java.util.List;
import java.util.Locale;
/**
* A {@link ParsedNdefMessage} consisting of one {@link UriRecord}.
*/
class UriMessage implements ParsedNdefMessage {
class UriMessage extends ParsedNdefMessage {
private final UriRecord mRecord;
UriMessage(UriRecord record) {
UriMessage(UriRecord record, List<ParsedNdefRecord> records) {
super(Preconditions.checkNotNull(records));
mRecord = Preconditions.checkNotNull(record);
}
@Override
public String getSnippet(Locale locale) {
public String getSnippet(Context context, Locale locale) {
// URIs cannot be localized
return mRecord.getUri().toString();
return mRecord.getPrettyUriString(context);
}
@Override

View File

@@ -16,6 +16,11 @@
package com.android.apps.tag.record;
import android.app.Activity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
/**
* TODO: come up with a better name.
*/
@@ -23,4 +28,9 @@ public interface ParsedNdefRecord {
// Just a placeholder for now. Probably not needed nor desired.
public String getRecordType();
/**
* Returns a view to display this record.
*/
public View getView(Activity activity, LayoutInflater inflater, ViewGroup parent);
}

View File

@@ -16,16 +16,25 @@
package com.android.apps.tag.record;
import android.nfc.FormatException;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import com.android.apps.tag.R;
import com.android.apps.tag.message.NdefMessageParser;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import android.app.Activity;
import android.nfc.FormatException;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.Arrays;
import java.util.NoSuchElementException;
import javax.annotation.Nullable;
/**
@@ -105,4 +114,23 @@ public class SmartPoster implements ParsedNdefRecord {
public String getRecordType() {
return "SmartPoster";
}
@Override
public View getView(Activity activity, LayoutInflater inflater, ViewGroup parent) {
if (mTitleRecord != null) {
// Build a container to hold the title and the URI
LinearLayout container = new LinearLayout(activity);
container.setOrientation(LinearLayout.VERTICAL);
container.setLayoutParams(new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
container.addView(mTitleRecord.getView(activity, inflater, container));
inflater.inflate(R.layout.tag_divider, container);
container.addView(mUriRecord.getView(activity, inflater, container));
return container;
} else {
// Just a URI, return a view for it directly
return mUriRecord.getView(activity, inflater, parent);
}
}
}

View File

@@ -16,10 +16,16 @@
package com.android.apps.tag.record;
import android.nfc.NdefRecord;
import com.android.apps.tag.R;
import com.google.common.base.Preconditions;
import android.app.Activity;
import android.nfc.NdefRecord;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
@@ -42,6 +48,13 @@ public class TextRecord implements ParsedNdefRecord {
return "Text";
}
@Override
public View getView(Activity activity, LayoutInflater inflater, ViewGroup parent) {
TextView text = (TextView) inflater.inflate(R.layout.tag_text, parent, false);
text.setText(mText);
return text;
}
public String getText() {
return mText;
}

View File

@@ -16,23 +16,49 @@
package com.android.apps.tag.record;
import android.net.Uri;
import android.nfc.NdefRecord;
import com.android.apps.tag.R;
import com.google.common.base.Preconditions;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.primitives.Bytes;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.nfc.NdefRecord;
import android.telephony.PhoneNumberUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.nio.charset.Charsets;
import java.util.Arrays;
import java.util.List;
/**
* A parsed record containing a Uri.
*/
public class UriRecord implements ParsedNdefRecord {
private static final byte[] EMPTY = new byte[0];
public class UriRecord implements ParsedNdefRecord, OnClickListener {
private static final class ClickInfo {
public Activity activity;
public Intent intent;
public ClickInfo(Activity activity, Intent intent) {
this.activity = activity;
this.intent = intent;
}
}
/**
* NFC Forum "URI Record Type Definition"
*
@@ -89,6 +115,96 @@ public class UriRecord implements ParsedNdefRecord {
return "Uri";
}
public Intent getIntentForUri() {
String scheme = mUri.getScheme();
if ("tel".equals(scheme)) {
return new Intent(Intent.ACTION_CALL, mUri);
} else if ("sms".equals(scheme) || "smsto".equals(scheme)) {
return new Intent(Intent.ACTION_SENDTO, mUri);
} else {
return new Intent(Intent.ACTION_VIEW, mUri);
}
}
public String getPrettyUriString(Context context) {
String scheme = mUri.getScheme();
boolean tel = "tel".equals(scheme);
boolean sms = "sms".equals(scheme) || "smsto".equals(scheme);
if (tel || sms) {
String ssp = mUri.getSchemeSpecificPart();
int offset = ssp.indexOf('?');
if (offset >= 0) {
ssp = ssp.substring(0, offset);
}
if (tel) {
return context.getString(R.string.action_call, PhoneNumberUtils.formatNumber(ssp));
} else {
return context.getString(R.string.action_text, PhoneNumberUtils.formatNumber(ssp));
}
} else {
return mUri.toString();
}
}
@Override
public View getView(Activity activity, LayoutInflater inflater, ViewGroup parent) {
Intent intent = getIntentForUri();
PackageManager pm = activity.getPackageManager();
List<ResolveInfo> activities = pm.queryIntentActivities(intent, 0);
int numActivities = activities.size();
if (numActivities == 0) {
TextView text = (TextView) inflater.inflate(R.layout.tag_text, parent, false);
text.setText(mUri.toString());
return text;
} else if (numActivities == 1) {
return buildActivityView(activity, activities.get(0), pm, inflater, parent);
} else {
// Build a container to hold the multiple entries
LinearLayout container = new LinearLayout(activity);
container.setOrientation(LinearLayout.VERTICAL);
container.setLayoutParams(new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
// Create an entry for each activity that can handle the URI
for (ResolveInfo resolveInfo : activities) {
if (container.getChildCount() > 0) {
inflater.inflate(R.layout.tag_divider, container);
}
container.addView(buildActivityView(activity, resolveInfo, pm, inflater, container));
}
return container;
}
}
private View buildActivityView(Activity activity, ResolveInfo resolveInfo, PackageManager pm,
LayoutInflater inflater, ViewGroup parent) {
Intent intent = getIntentForUri();
ActivityInfo activityInfo = resolveInfo.activityInfo;
intent.setComponent(new ComponentName(activityInfo.packageName, activityInfo.name));
View item = inflater.inflate(R.layout.tag_uri, parent, false);
item.setOnClickListener(this);
item.setTag(new ClickInfo(activity, intent));
ImageView icon = (ImageView) item.findViewById(R.id.icon);
icon.setImageDrawable(resolveInfo.loadIcon(pm));
TextView text = (TextView) item.findViewById(R.id.secondary);
text.setText(resolveInfo.loadLabel(pm));
text = (TextView) item.findViewById(R.id.primary);
text.setText(getPrettyUriString(activity));
return item;
}
@Override
public void onClick(View view) {
ClickInfo info = (ClickInfo) view.getTag();
info.activity.startActivity(info.intent);
info.activity.finish();
}
public Uri getUri() {
return mUri;
}