Add context menu (long press) on items
[android/platform/packages/apps/Tag.git] / src / com / android / apps / tag / MyTagList.java
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16
17 package com.android.apps.tag;
18
19 import com.android.apps.tag.provider.TagContract.NdefMessages;
20 import com.google.common.base.Preconditions;
21 import com.google.common.collect.Lists;
22
23 import android.app.Activity;
24 import android.app.AlertDialog;
25 import android.app.Dialog;
26 import android.content.ContentUris;
27 import android.content.Context;
28 import android.content.DialogInterface;
29 import android.content.Intent;
30 import android.content.SharedPreferences;
31 import android.database.CharArrayBuffer;
32 import android.database.Cursor;
33 import android.nfc.FormatException;
34 import android.nfc.NdefMessage;
35 import android.nfc.NfcAdapter;
36 import android.os.AsyncTask;
37 import android.os.Build;
38 import android.os.Bundle;
39 import android.util.Log;
40 import android.view.ContextMenu;
41 import android.view.ContextMenu.ContextMenuInfo;
42 import android.view.LayoutInflater;
43 import android.view.Menu;
44 import android.view.MenuInflater;
45 import android.view.MenuItem;
46 import android.view.View;
47 import android.view.ViewGroup;
48 import android.widget.AdapterView;
49 import android.widget.AdapterView.AdapterContextMenuInfo;
50 import android.widget.AdapterView.OnItemClickListener;
51 import android.widget.CheckBox;
52 import android.widget.CursorAdapter;
53 import android.widget.ListView;
54 import android.widget.SimpleAdapter;
55 import android.widget.TextView;
56 import android.widget.Toast;
57
58 import java.lang.ref.WeakReference;
59 import java.util.ArrayList;
60 import java.util.HashMap;
61
62 /**
63  * Displays the list of tags that can be set as "My tag", and allows the user to select the
64  * active tag that the device shares.
65  */
66 public class MyTagList extends Activity implements OnItemClickListener, View.OnClickListener {
67
68     static final String TAG = "TagList";
69
70     private static final int REQUEST_EDIT = 0;
71     private static final int DIALOG_ID_SELECT_ACTIVE_TAG = 0;
72
73     private static final String BUNDLE_KEY_TAG_ID_IN_EDIT = "tag-edit";
74     private static final String PREF_KEY_ACTIVE_TAG = "active-my-tag";
75     static final String PREF_KEY_TAG_TO_WRITE = "tag-to-write";
76
77     private View mSelectActiveTagAnchor;
78     private View mActiveTagDetails;
79     private CheckBox mEnabled;
80     private ListView mList;
81
82     private TagAdapter mAdapter;
83     private long mActiveTagId;
84     private NdefMessage mActiveTag;
85
86     private WeakReference<SelectActiveTagDialog> mSelectActiveTagDialog;
87     private long mTagIdInEdit = -1;
88     private long mTagIdLongPressed;
89
90     private boolean mWriteSupport = false;
91
92     @Override
93     public void onCreate(Bundle savedInstanceState) {
94         super.onCreate(savedInstanceState);
95
96         setContentView(R.layout.my_tag_activity);
97
98         if (savedInstanceState != null) {
99             mTagIdInEdit = savedInstanceState.getLong(BUNDLE_KEY_TAG_ID_IN_EDIT, -1);
100         }
101
102         // Set up the check box to toggle My tag sharing.
103         mEnabled = (CheckBox) findViewById(R.id.toggle_enabled_checkbox);
104         mEnabled.setChecked(false);  // Set after initial data load completes.
105         findViewById(R.id.toggle_enabled_target).setOnClickListener(this);
106
107         // Setup the active tag selector.
108         mActiveTagDetails = findViewById(R.id.active_tag_details);
109         mSelectActiveTagAnchor = findViewById(R.id.choose_my_tag);
110         findViewById(R.id.active_tag).setOnClickListener(this);
111         updateActiveTagView(null);  // Filled in after initial data load.
112
113         mActiveTagId = getPreferences(Context.MODE_PRIVATE).getLong(PREF_KEY_ACTIVE_TAG, -1);
114
115         // Setup the list
116         mAdapter = new TagAdapter(this);
117         mList = (ListView) findViewById(android.R.id.list);
118         mList.setAdapter(mAdapter);
119         mList.setOnItemClickListener(this);
120         findViewById(R.id.add_tag).setOnClickListener(this);
121
122         // Don't setup the empty view until after the first load
123         // so the empty text doesn't flash when first loading the
124         // activity.
125         mList.setEmptyView(null);
126
127         // Kick off an async task to load the tags.
128         new TagLoaderTask().execute((Void[]) null);
129
130         // If we're not on a user build offer a back door for writing tags.
131         // The UX is horrible so we don't want to ship it but need it for testing.
132         if (!Build.TYPE.equalsIgnoreCase("user")) {
133             mWriteSupport = true;
134             registerForContextMenu(mList);
135         }
136     }
137
138     @Override
139     protected void onRestart() {
140         super.onRestart();
141         mTagIdInEdit = -1;
142     }
143
144     @Override
145     protected void onSaveInstanceState(Bundle outState) {
146         super.onSaveInstanceState(outState);
147         outState.putLong(BUNDLE_KEY_TAG_ID_IN_EDIT, mTagIdInEdit);
148     }
149
150     @Override
151     protected void onDestroy() {
152         if (mAdapter != null) {
153             mAdapter.changeCursor(null);
154         }
155         super.onDestroy();
156     }
157
158
159     @Override
160     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
161         editTag(id);
162     }
163
164     /**
165      * Opens the tag editor for a particular tag.
166      */
167     private void editTag(long id) {
168         // TODO: use implicit Intent?
169         Intent intent = new Intent(this, EditTagActivity.class);
170         intent.setData(ContentUris.withAppendedId(NdefMessages.CONTENT_URI, id));
171         mTagIdInEdit = id;
172         startActivityForResult(intent, REQUEST_EDIT);
173     }
174
175     public void setEmptyView() {
176         // TODO: set empty view.
177     }
178
179     public interface TagQuery {
180         static final String[] PROJECTION = new String[] {
181                 NdefMessages._ID, // 0
182                 NdefMessages.DATE, // 1
183                 NdefMessages.TITLE, // 2
184                 NdefMessages.BYTES, // 3
185         };
186
187         static final int COLUMN_ID = 0;
188         static final int COLUMN_DATE = 1;
189         static final int COLUMN_TITLE = 2;
190         static final int COLUMN_BYTES = 3;
191     }
192
193     /**
194      * Asynchronously loads the tags info from the database.
195      */
196     final class TagLoaderTask extends AsyncTask<Void, Void, Cursor> {
197         @Override
198         public Cursor doInBackground(Void... args) {
199             Cursor cursor = getContentResolver().query(
200                     NdefMessages.CONTENT_URI,
201                     TagQuery.PROJECTION,
202                     NdefMessages.IS_MY_TAG + "=1",
203                     null, NdefMessages.DATE + " DESC");
204
205             // Ensure the cursor executes and fills its window
206             if (cursor != null) cursor.getCount();
207             return cursor;
208         }
209
210         @Override
211         protected void onPostExecute(Cursor cursor) {
212             mAdapter.changeCursor(cursor);
213
214             if (cursor == null || cursor.getCount() == 0) {
215                 setEmptyView();
216             } else {
217                 // Find the active tag.
218                 if (mActiveTagId != -1) {
219                     cursor.moveToPosition(-1);
220                     while (cursor.moveToNext()) {
221                         if (mActiveTagId == cursor.getLong(TagQuery.COLUMN_ID)) {
222                             selectActiveTag(cursor.getPosition());
223
224                             // If there was an existing shared tag, we update the contents, since
225                             // the active tag contents may have been changed. This also forces the
226                             // active tag to be in sync with what the NfcAdapter.
227                             if (NfcAdapter.getDefaultAdapter(MyTagList.this)
228                                     .getLocalNdefMessage() != null) {
229                                 enableSharing();
230                             }
231                             break;
232                         }
233                     }
234                 }
235             }
236
237
238             SelectActiveTagDialog dialog = (mSelectActiveTagDialog == null)
239                     ? null : mSelectActiveTagDialog.get();
240             if (dialog != null) {
241                 dialog.setData(cursor);
242             }
243         }
244     }
245
246     /**
247      * Struct to hold pointers to views in the list items to save time at view binding time.
248      */
249     static final class ViewHolder {
250         public CharArrayBuffer titleBuffer;
251         public TextView mainLine;
252     }
253
254     /**
255      * Adapter to display the the My tag entries.
256      */
257     public class TagAdapter extends CursorAdapter {
258         private final LayoutInflater mInflater;
259
260         public TagAdapter(Context context) {
261             super(context, null, false);
262             mInflater = LayoutInflater.from(context);
263         }
264
265         @Override
266         public void bindView(View view, Context context, Cursor cursor) {
267             ViewHolder holder = (ViewHolder) view.getTag();
268
269             CharArrayBuffer buf = holder.titleBuffer;
270             cursor.copyStringToBuffer(TagQuery.COLUMN_TITLE, buf);
271             holder.mainLine.setText(buf.data, 0, buf.sizeCopied);
272         }
273
274         @Override
275         public View newView(Context context, Cursor cursor, ViewGroup parent) {
276             View view = mInflater.inflate(R.layout.tag_list_item, null);
277
278             // Cache items for the view
279             ViewHolder holder = new ViewHolder();
280             holder.titleBuffer = new CharArrayBuffer(64);
281             holder.mainLine = (TextView) view.findViewById(R.id.title);
282             view.findViewById(R.id.date).setVisibility(View.GONE);
283             view.setTag(holder);
284
285             return view;
286         }
287
288         @Override
289         public void onContentChanged() {
290             // Kick off an async query to refresh the list
291             new TagLoaderTask().execute((Void[]) null);
292         }
293     }
294
295     @Override
296     public void onClick(View target) {
297         switch (target.getId()) {
298             case R.id.toggle_enabled_target:
299                 boolean enabled = !mEnabled.isChecked();
300                 if (enabled) {
301                     if (mActiveTag != null) {
302                         enableSharing();
303                         return;
304                     }
305                     // TODO: just disable the checkbox when no tag is set
306                     Toast.makeText(
307                             this,
308                             "You must select a tag to share first.",
309                             Toast.LENGTH_SHORT).show();
310                 }
311
312                 disableSharing();
313                 break;
314
315             case R.id.add_tag:
316                 // TODO: use implicit intents.
317                 Intent intent = new Intent(this, EditTagActivity.class);
318                 startActivityForResult(intent, REQUEST_EDIT);
319                 break;
320
321             case R.id.active_tag:
322                 showDialog(DIALOG_ID_SELECT_ACTIVE_TAG);
323                 break;
324         }
325     }
326
327     @Override
328     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) {
329         Cursor cursor = mAdapter.getCursor();
330         if (cursor == null
331                 || cursor.isClosed()
332                 || !cursor.moveToPosition(((AdapterContextMenuInfo) info).position)) {
333             return;
334         }
335
336         long id = cursor.getLong(TagQuery.COLUMN_ID);
337         MenuInflater inflater = getMenuInflater();
338         inflater.inflate(R.menu.my_tag_list_context_menu, menu);
339
340         // Prepare the menu for the item.
341         menu.findItem(R.id.set_as_active).setVisible(id != mActiveTagId);
342         mTagIdLongPressed = id;
343
344         if (mWriteSupport) {
345             menu.add(0, 1, 0, "Write to next tag scanned");
346         }
347     }
348
349     @Override
350     public boolean onContextItemSelected(MenuItem item) {
351         long id = mTagIdLongPressed;
352         switch (item.getItemId()) {
353             case R.id.delete:
354                 deleteTag(id);
355                 return true;
356
357             case R.id.set_as_active:
358                 Cursor cursor = mAdapter.getCursor();
359                 if (cursor == null || cursor.isClosed()) {
360                     break;
361                 }
362
363                 for (int position = 0; cursor.moveToPosition(position); position++) {
364                     if (cursor.getLong(TagQuery.COLUMN_ID) == id) {
365                         selectActiveTag(position);
366                         return true;
367                     }
368                 }
369                 break;
370
371             case R.id.edit:
372                 editTag(id);
373                 return true;
374
375             case 1:
376                 AdapterView.AdapterContextMenuInfo info;
377                 try {
378                     info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
379                 } catch (ClassCastException e) {
380                     Log.e(TAG, "bad menuInfo", e);
381                     break;
382                 }
383
384                 SharedPreferences prefs = getSharedPreferences("tags.pref", Context.MODE_PRIVATE);
385                 prefs.edit().putLong(PREF_KEY_TAG_TO_WRITE, info.id).apply();
386                 return true;
387         }
388         return false;
389     }
390
391     @Override
392     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
393         if (requestCode == REQUEST_EDIT && resultCode == RESULT_OK) {
394             NdefMessage msg = (NdefMessage) Preconditions.checkNotNull(
395                     data.getParcelableExtra(EditTagActivity.EXTRA_RESULT_MSG));
396
397             if (mTagIdInEdit != -1) {
398                 TagService.updateMyMessage(this, mTagIdInEdit, msg);
399             } else {
400                 TagService.saveMyMessages(this, new NdefMessage[] { msg });
401             }
402         }
403     }
404
405     @Override
406     protected Dialog onCreateDialog(int id, Bundle args) {
407         if (id == DIALOG_ID_SELECT_ACTIVE_TAG) {
408             mSelectActiveTagDialog = new WeakReference<SelectActiveTagDialog>(
409                     new SelectActiveTagDialog(this, mAdapter.getCursor()));
410             return mSelectActiveTagDialog.get();
411         }
412         return super.onCreateDialog(id, args);
413     }
414
415     /**
416      * Selects the tag to be used as the "My tag" shared tag.
417      *
418      * This does not necessarily persist the selection to the {@code NfcAdapter}. That must be done
419      * via {@link #enableSharing}. However, it will call {@link #disableSharing} if the tag
420      * is invalid.
421      */
422     private void selectActiveTag(int position) {
423         Cursor cursor = mAdapter.getCursor();
424         if (cursor != null && cursor.moveToPosition(position)) {
425             mActiveTagId = cursor.getLong(TagQuery.COLUMN_ID);
426
427             try {
428                 mActiveTag = new NdefMessage(cursor.getBlob(TagQuery.COLUMN_BYTES));
429
430                 // Persist active tag info to preferences.
431                 getPreferences(Context.MODE_PRIVATE)
432                         .edit()
433                         .putLong(PREF_KEY_ACTIVE_TAG, mActiveTagId)
434                         .apply();
435
436                 // Notify NFC adapter of the My tag contents.
437                 updateActiveTagView(cursor.getString(TagQuery.COLUMN_TITLE));
438
439             } catch (FormatException e) {
440                 // TODO: handle.
441                 disableSharing();
442             }
443         } else {
444             updateActiveTagView(null);
445             disableSharing();
446         }
447     }
448
449     private void enableSharing() {
450         mEnabled.setChecked(true);
451         NfcAdapter.getDefaultAdapter(this).setLocalNdefMessage(
452             Preconditions.checkNotNull(mActiveTag));
453     }
454
455     private void disableSharing() {
456         mEnabled.setChecked(false);
457         NfcAdapter.getDefaultAdapter(this).setLocalNdefMessage(null);
458     }
459
460     private void updateActiveTagView(String title) {
461         if (title == null) {
462             mActiveTagDetails.setVisibility(View.GONE);
463             mSelectActiveTagAnchor.setVisibility(View.VISIBLE);
464         } else {
465             mActiveTagDetails.setVisibility(View.VISIBLE);
466             ((TextView) mActiveTagDetails.findViewById(R.id.active_tag_title)).setText(title);
467             mSelectActiveTagAnchor.setVisibility(View.GONE);
468         }
469     }
470
471     /**
472      * Removes the tag from the "My tag" list.
473      */
474     private void deleteTag(long id) {
475         if (id == mActiveTagId) {
476             selectActiveTag(-1);
477         }
478         TagService.delete(this, ContentUris.withAppendedId(NdefMessages.CONTENT_URI, id));
479     }
480
481     class SelectActiveTagDialog extends AlertDialog
482             implements DialogInterface.OnClickListener, OnItemClickListener {
483
484         private final ArrayList<HashMap<String, String>> mData;
485         private final SimpleAdapter mSelectAdapter;
486
487         protected SelectActiveTagDialog(Context context, Cursor cursor) {
488             super(context);
489
490             setTitle(context.getResources().getString(R.string.choose_my_tag));
491             ListView list = new ListView(MyTagList.this);
492
493             mData = Lists.newArrayList();
494             mSelectAdapter = new SimpleAdapter(
495                     context,
496                     mData,
497                     android.R.layout.simple_list_item_1,
498                     new String[] { "title" },
499                     new int[] { android.R.id.text1 });
500
501             list.setAdapter(mSelectAdapter);
502             list.setOnItemClickListener(this);
503             setView(list);
504             setIcon(0);
505             setButton(
506                     DialogInterface.BUTTON_POSITIVE,
507                     context.getString(android.R.string.cancel),
508                     this);
509
510             setData(cursor);
511         }
512
513         public void setData(final Cursor cursor) {
514             if ((cursor == null) || (cursor.getCount() == 0)) {
515                 cancel();
516                 return;
517             }
518             mData.clear();
519
520             cursor.moveToPosition(-1);
521             while (cursor.moveToNext()) {
522                 mData.add(new HashMap<String, String>() {{
523                     put("title", cursor.getString(MyTagList.TagQuery.COLUMN_TITLE));
524                 }});
525             }
526
527             mSelectAdapter.notifyDataSetChanged();
528         }
529
530         @Override
531         public void onClick(DialogInterface dialog, int which) {
532             cancel();
533         }
534
535         @Override
536         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
537             selectActiveTag(position);
538             enableSharing();
539             cancel();
540         }
541     }
542
543 }