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