blob: 9186389712b08f2895be114e78e4edee12ea41ad [file] [log] [blame]
/*
* 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.provider.TagContract.NdefMessages;
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.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import android.app.Activity;
import android.app.AlertDialog;
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.database.CharArrayBuffer;
import android.database.Cursor;
import android.net.Uri;
import android.nfc.FormatException;
import android.nfc.NdefMessage;
import android.nfc.NfcAdapter;
import android.os.AsyncTask;
import android.os.Build;
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;
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,
TagService.SaveCallbacks,
DialogInterface.OnClickListener {
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";
static final String[] SUPPORTED_TYPES = new String[] {
VCardRecord.RECORD_TYPE,
UriRecord.RECORD_TYPE,
TextRecord.RECORD_TYPE,
};
private View mSelectActiveTagAnchor;
private View mActiveTagDetails;
private CheckBox mEnabled;
private ListView mList;
private TagAdapter mAdapter;
private long mActiveTagId;
private Uri mTagBeingSaved;
private NdefMessage mActiveTag;
private WeakReference<SelectActiveTagDialog> mSelectActiveTagDialog;
private long mTagIdInEdit = -1;
private long mTagIdLongPressed;
private boolean mWriteSupport = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.my_tag_activity);
if (savedInstanceState != null) {
mTagIdInEdit = savedInstanceState.getLong(BUNDLE_KEY_TAG_ID_IN_EDIT, -1);
}
// Set up the check box to toggle My tag sharing.
mEnabled = (CheckBox) findViewById(R.id.toggle_enabled_checkbox);
mEnabled.setChecked(false); // Set after initial data load completes.
findViewById(R.id.toggle_enabled_target).setOnClickListener(this);
// Setup the active tag selector.
mActiveTagDetails = findViewById(R.id.active_tag_details);
mSelectActiveTagAnchor = findViewById(R.id.choose_my_tag);
findViewById(R.id.active_tag).setOnClickListener(this);
updateActiveTagView(null); // Filled in after initial data load.
mActiveTagId = getPreferences(Context.MODE_PRIVATE).getLong(PREF_KEY_ACTIVE_TAG, -1);
// Setup the list.
mAdapter = new TagAdapter(this);
mList = (ListView) findViewById(android.R.id.list);
mList.setAdapter(mAdapter);
mList.setOnItemClickListener(this);
findViewById(R.id.add_tag).setOnClickListener(this);
// Don't setup the empty view until after the first load
// so the empty text doesn't flash when first loading the
// activity.
mList.setEmptyView(null);
// Kick off an async task to load the tags.
new TagLoaderTask().execute((Void[]) null);
// If we're not on a user build offer a back door for writing tags.
// The UX is horrible so we don't want to ship it but need it for testing.
if (!Build.TYPE.equalsIgnoreCase("user")) {
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
protected void onRestart() {
super.onRestart();
mTagIdInEdit = -1;
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putLong(BUNDLE_KEY_TAG_ID_IN_EDIT, mTagIdInEdit);
}
@Override
protected void onDestroy() {
if (mAdapter != null) {
mAdapter.changeCursor(null);
}
super.onDestroy();
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
editTag(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));
mTagIdInEdit = id;
startActivityForResult(intent, REQUEST_EDIT);
}
public void setEmptyView() {
// TODO: set empty view.
}
public interface TagQuery {
static final String[] PROJECTION = new String[] {
NdefMessages._ID, // 0
NdefMessages.DATE, // 1
NdefMessages.TITLE, // 2
NdefMessages.BYTES, // 3
};
static final int COLUMN_ID = 0;
static final int COLUMN_DATE = 1;
static final int COLUMN_TITLE = 2;
static final int COLUMN_BYTES = 3;
}
/**
* Asynchronously loads the tags info from the database.
*/
final class TagLoaderTask extends AsyncTask<Void, Void, Cursor> {
@Override
public Cursor doInBackground(Void... args) {
Cursor cursor = getContentResolver().query(
NdefMessages.CONTENT_URI,
TagQuery.PROJECTION,
NdefMessages.IS_MY_TAG + "=1",
null, NdefMessages.DATE + " DESC");
// Ensure the cursor executes and fills its window
if (cursor != null) cursor.getCount();
return cursor;
}
@Override
protected void onPostExecute(Cursor cursor) {
mAdapter.changeCursor(cursor);
if (cursor == null || cursor.getCount() == 0) {
setEmptyView();
} else {
// Find the active tag.
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;
}
}
}
}
SelectActiveTagDialog dialog = (mSelectActiveTagDialog == null)
? null : mSelectActiveTagDialog.get();
if (dialog != null) {
dialog.setData(cursor);
}
}
}
/**
* Struct to hold pointers to views in the list items to save time at view binding time.
*/
static final class ViewHolder {
public CharArrayBuffer titleBuffer;
public TextView mainLine;
public ImageView activeIcon;
}
/**
* Adapter to display the the My tag entries.
*/
public class TagAdapter extends CursorAdapter {
private final LayoutInflater mInflater;
public TagAdapter(Context context) {
super(context, null, false);
mInflater = LayoutInflater.from(context);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
ViewHolder holder = (ViewHolder) view.getTag();
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
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View view = mInflater.inflate(R.layout.tag_list_item, null);
// Cache items for the view
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);
return view;
}
@Override
public void onContentChanged() {
// Kick off an async query to refresh the list
new TagLoaderTask().execute((Void[]) null);
}
}
@Override
public void onClick(View target) {
switch (target.getId()) {
case R.id.toggle_enabled_target:
boolean enabled = !mEnabled.isChecked();
if (enabled) {
if (mActiveTag != null) {
enableSharingAndStoreTag();
return;
}
Toast.makeText(
this,
getResources().getString(R.string.no_tag_selected),
Toast.LENGTH_SHORT).show();
}
disableSharing();
break;
case R.id.add_tag:
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 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(
data.getParcelableExtra(EditTagActivity.EXTRA_RESULT_MSG));
if (mTagIdInEdit != -1) {
TagService.updateMyMessage(this, mTagIdInEdit, msg);
} else {
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) {
Context lightTheme = new ContextThemeWrapper(this, android.R.style.Theme_Light);
if (id == DIALOG_ID_SELECT_ACTIVE_TAG) {
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) {
ContentSelectorAdapter adapter = new ContentSelectorAdapter(lightTheme,
SUPPORTED_TYPES);
AlertDialog dialog = new AlertDialog.Builder(lightTheme)
.setTitle(R.string.select_type)
.setIcon(0)
.setNegativeButton(android.R.string.cancel, this)
.setAdapter(adapter, this)
.create();
adapter.setListView(dialog.getListView());
dialog.setInverseBackgroundForced(true);
return dialog;
}
return super.onCreateDialog(id, args);
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_NEGATIVE) {
dialog.cancel();
} else {
RecordEditInfo info = (RecordEditInfo) ((AlertDialog) dialog).getListView()
.getAdapter().getItem(which);
Intent intent = new Intent(this, EditTagActivity.class);
intent.putExtra(EditTagActivity.EXTRA_NEW_RECORD_INFO, info);
startActivityForResult(intent, REQUEST_EDIT);
}
}
/**
* 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 #enableSharingAndStoreTag()}. However, it will call {@link #disableSharing()}
* if the tag is invalid.
*/
private void selectActiveTag(int position) {
Cursor cursor = mAdapter.getCursor();
if (cursor != null && cursor.moveToPosition(position)) {
mActiveTagId = cursor.getLong(TagQuery.COLUMN_ID);
try {
mActiveTag = new NdefMessage(cursor.getBlob(TagQuery.COLUMN_BYTES));
// Persist active tag info to preferences.
getPreferences(Context.MODE_PRIVATE)
.edit()
.putLong(PREF_KEY_ACTIVE_TAG, mActiveTagId)
.apply();
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.
disableSharing();
}
} else {
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 enableSharingAndStoreTag() {
mEnabled.setChecked(true);
NfcAdapter.getDefaultAdapter(this).setLocalNdefMessage(
Preconditions.checkNotNull(mActiveTag));
}
private void disableSharing() {
mEnabled.setChecked(false);
NfcAdapter.getDefaultAdapter(this).setLocalNdefMessage(null);
}
private void updateActiveTagView(String title) {
if (title == null) {
mActiveTagDetails.setVisibility(View.GONE);
mSelectActiveTagAnchor.setVisibility(View.VISIBLE);
} else {
mActiveTagDetails.setVisibility(View.VISIBLE);
((TextView) mActiveTagDetails.findViewById(R.id.active_tag_title)).setText(title);
mSelectActiveTagAnchor.setVisibility(View.GONE);
}
}
/**
* 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 {
private final ArrayList<HashMap<String, String>> mData;
private final SimpleAdapter mSelectAdapter;
protected SelectActiveTagDialog(Context context, Cursor cursor) {
super(context);
setTitle(context.getResources().getString(R.string.choose_my_tag));
ListView list = new ListView(context);
mData = Lists.newArrayList();
mSelectAdapter = new SimpleAdapter(
context,
mData,
android.R.layout.simple_list_item_1,
new String[] { "title" },
new int[] { android.R.id.text1 });
list.setAdapter(mSelectAdapter);
list.setOnItemClickListener(this);
setView(list);
setIcon(0);
setButton(
DialogInterface.BUTTON_POSITIVE,
context.getString(android.R.string.cancel),
this);
setData(cursor);
}
public void setData(final Cursor cursor) {
if ((cursor == null) || (cursor.getCount() == 0)) {
cancel();
return;
}
mData.clear();
cursor.moveToPosition(-1);
while (cursor.moveToNext()) {
mData.add(new HashMap<String, String>() {{
put("title", cursor.getString(MyTagList.TagQuery.COLUMN_TITLE));
}});
}
mSelectAdapter.notifyDataSetChanged();
}
@Override
public void onClick(DialogInterface dialog, int which) {
cancel();
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
selectActiveTag(position);
enableSharingAndStoreTag();
cancel();
}
}
}