TagViewer.java revision c27e0b7e8c87db872bf3fd7ceda169ad725be33c
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
17package com.android.apps.tag;
18
19import com.android.apps.tag.message.NdefMessageParser;
20import com.android.apps.tag.message.ParsedNdefMessage;
21import com.android.apps.tag.provider.TagContract.NdefMessages;
22import com.android.apps.tag.record.ParsedNdefRecord;
23
24import android.app.Activity;
25import android.app.PendingIntent;
26import android.content.BroadcastReceiver;
27import android.content.ContentUris;
28import android.content.Context;
29import android.content.Intent;
30import android.content.IntentFilter;
31import android.content.SharedPreferences;
32import android.content.res.AssetFileDescriptor;
33import android.database.Cursor;
34import android.media.AudioManager;
35import android.media.MediaPlayer;
36import android.net.Uri;
37import android.nfc.FormatException;
38import android.nfc.NdefMessage;
39import android.nfc.NdefRecord;
40import android.nfc.NfcAdapter;
41import android.nfc.Tag;
42import android.nfc.technology.Ndef;
43import android.nfc.technology.NdefFormatable;
44import android.nfc.technology.TagTechnology;
45import android.os.AsyncTask;
46import android.os.Bundle;
47import android.os.Parcelable;
48import android.os.PowerManager;
49import android.os.PowerManager.WakeLock;
50import android.text.format.DateUtils;
51import android.util.Log;
52import android.view.LayoutInflater;
53import android.view.View;
54import android.view.View.OnClickListener;
55import android.view.WindowManager;
56import android.widget.Button;
57import android.widget.CheckBox;
58import android.widget.ImageView;
59import android.widget.LinearLayout;
60import android.widget.TextView;
61import android.widget.Toast;
62
63import java.io.IOException;
64import java.util.List;
65
66/**
67 * An {@link Activity} which handles a broadcast of a new tag that the device just discovered.
68 */
69public class TagViewer extends Activity implements OnClickListener {
70    static final String TAG = "SaveTag";
71    static final String EXTRA_TAG_DB_ID = "db_id";
72    static final String EXTRA_MESSAGE = "msg";
73    static final String EXTRA_KEEP_TITLE = "keepTitle";
74
75    static final boolean SHOW_OVER_LOCK_SCREEN = false;
76
77    /** This activity will finish itself in this amount of time if the user doesn't do anything. */
78    static final int ACTIVITY_TIMEOUT_MS = 7 * 1000;
79
80    Uri mTagUri;
81    ImageView mIcon;
82    TextView mTitle;
83    TextView mDate;
84    CheckBox mStar;
85    Button mDeleteButton;
86    Button mDoneButton;
87    LinearLayout mTagContent;
88
89    BroadcastReceiver mReceiver;
90
91    private class ScreenOffReceiver extends BroadcastReceiver {
92        @Override
93        public void onReceive(Context context, Intent intent) {
94            if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
95                if (!isFinishing()) {
96                    finish();
97                }
98            }
99        }
100    }
101
102    @Override
103    protected void onCreate(Bundle savedInstanceState) {
104        super.onCreate(savedInstanceState);
105
106        if (SHOW_OVER_LOCK_SCREEN) {
107            getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
108        }
109
110        setContentView(R.layout.tag_viewer);
111
112        mTagContent = (LinearLayout) findViewById(R.id.list);
113        mTitle = (TextView) findViewById(R.id.title);
114        mDate = (TextView) findViewById(R.id.date);
115        mIcon = (ImageView) findViewById(R.id.icon);
116        mStar = (CheckBox) findViewById(R.id.star);
117        mDeleteButton = (Button) findViewById(R.id.button_delete);
118        mDoneButton = (Button) findViewById(R.id.button_done);
119
120        mDeleteButton.setOnClickListener(this);
121        mDoneButton.setOnClickListener(this);
122        mStar.setOnClickListener(this);
123        mIcon.setImageResource(R.drawable.ic_launcher_nfc);
124
125        resolveIntent(getIntent());
126    }
127
128    @Override
129    public void onRestart() {
130        super.onRestart();
131        if (mTagUri == null) {
132            // Someone how the user was fast enough to navigate away from the activity
133            // before the service was able to save the tag and call back onto this
134            // activity with the pending intent. Since we don't know what do display here
135            // just finish the activity.
136            finish();
137        }
138    }
139
140    @Override
141    public void onStop() {
142        super.onStop();
143
144        PendingIntent pending = getPendingIntent();
145        pending.cancel();
146
147        if (mReceiver != null) {
148            unregisterReceiver(mReceiver);
149            mReceiver = null;
150        }
151    }
152
153    private PendingIntent getPendingIntent() {
154        Intent callback = new Intent();
155        callback.setClass(this, TagViewer.class);
156        callback.setAction(Intent.ACTION_VIEW);
157        callback.setFlags(Intent. FLAG_ACTIVITY_CLEAR_TOP);
158        callback.putExtra(EXTRA_KEEP_TITLE, true);
159
160        return PendingIntent.getActivity(this, 0, callback, PendingIntent.FLAG_CANCEL_CURRENT);
161    }
162
163    void resolveIntent(Intent intent) {
164        // Parse the intent
165        String action = intent.getAction();
166        if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)
167                || NfcAdapter.ACTION_TECHNOLOGY_DISCOVERED.equals(action)) {
168            if (SHOW_OVER_LOCK_SCREEN) {
169                // A tag was just scanned so poke the user activity wake lock to keep
170                // the screen on a bit longer in the event that the activity has
171                // hidden the lock screen.
172                PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
173                WakeLock wakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, TAG);
174                // This lock CANNOT be manually released in onStop() since that may
175                // cause a lock under run exception to be thrown when the timeout
176                // hits.
177                wakeLock.acquire(ACTIVITY_TIMEOUT_MS);
178
179                if (mReceiver == null) {
180                    mReceiver = new ScreenOffReceiver();
181                    IntentFilter filter = new IntentFilter();
182                    filter.addAction(Intent.ACTION_SCREEN_OFF);
183                    registerReceiver(mReceiver, filter);
184                }
185            }
186
187            // Check to see if there's a tag queued up for writing.
188            SharedPreferences prefs = getSharedPreferences("tags.pref", Context.MODE_PRIVATE);
189            long tagToWrite = prefs.getLong(MyTagList.PREF_KEY_TAG_TO_WRITE, 0);
190            prefs.edit().putLong(MyTagList.PREF_KEY_TAG_TO_WRITE, 0).apply();
191            if (tagToWrite != 0) {
192                if (writeTag((Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG), tagToWrite)) {
193                    Toast.makeText(this, "Tag written", Toast.LENGTH_SHORT).show();
194                    finish();
195                    return;
196                }
197            }
198
199            // When a tag is discovered we send it to the service to be save. We
200            // include a PendingIntent for the service to call back onto. This
201            // will cause this activity to be restarted with onNewIntent(). At
202            // that time we read it from the database and view it.
203            Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
204            NdefMessage[] msgs;
205            if (rawMsgs != null && rawMsgs.length > 0) {
206                // stupid java, need to cast one-by-one
207                msgs = new NdefMessage[rawMsgs.length];
208                for (int i=0; i<rawMsgs.length; i++) {
209                    msgs[i] = (NdefMessage) rawMsgs[i];
210                }
211            } else {
212                // Unknown tag type
213                byte[] empty = new byte[] {};
214                NdefRecord record = new NdefRecord(NdefRecord.TNF_UNKNOWN, empty, empty, empty);
215                NdefMessage msg = new NdefMessage(new NdefRecord[] { record });
216                msgs = new NdefMessage[] { msg };
217            }
218            TagService.saveMessages(this, msgs, false, getPendingIntent());
219
220            // Setup the views
221            setTitle(R.string.title_scanned_tag);
222            mDate.setVisibility(View.GONE);
223            mStar.setChecked(false);
224            mStar.setEnabled(true);
225
226            // Play notification.
227            try {
228                AssetFileDescriptor afd = getResources().openRawResourceFd(
229                        R.raw.discovered_tag_notification);
230                if (afd != null) {
231                    MediaPlayer player = new MediaPlayer();
232                    player.setDataSource(
233                            afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
234                    afd.close();
235                    player.setAudioStreamType(AudioManager.STREAM_NOTIFICATION);
236                    player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
237                        @Override
238                        public void onCompletion(MediaPlayer mp) {
239                            mp.release();
240                        }
241                    });
242                    player.prepare();
243                    player.start();
244                }
245            } catch (IOException ex) {
246                Log.d(TAG, "Unable to play sound for tag discovery", ex);
247            } catch (IllegalArgumentException ex) {
248                Log.d(TAG, "Unable to play sound for tag discovery", ex);
249            } catch (SecurityException ex) {
250                Log.d(TAG, "Unable to play sound for tag discovery", ex);
251            }
252
253        } else if (Intent.ACTION_VIEW.equals(action)) {
254            // Setup the views
255            if (!intent.getBooleanExtra(EXTRA_KEEP_TITLE, false)) {
256                setTitle(R.string.title_existing_tag);
257                mDate.setVisibility(View.VISIBLE);
258            }
259
260            mStar.setVisibility(View.VISIBLE);
261            mStar.setEnabled(false); // it's reenabled when the async load completes
262
263            // Read the tag from the database asynchronously
264            mTagUri = intent.getData();
265            new LoadTagTask().execute(mTagUri);
266        } else {
267            Log.e(TAG, "Unknown intent " + intent);
268            finish();
269            return;
270        }
271    }
272
273    private boolean writeTag(Tag tag, long id) {
274        try {
275            NfcAdapter adapter = NfcAdapter.getDefaultAdapter(this);
276            Cursor cursor = getContentResolver().query(
277                    ContentUris.withAppendedId(NdefMessages.CONTENT_URI, id),
278                    new String[] { NdefMessages.BYTES }, null, null, null);
279            if (cursor == null || !cursor.moveToFirst()) {
280                return false;
281            }
282
283            byte[] bytes = cursor.getBlob(0);
284            cursor.close();
285            NdefMessage msg = new NdefMessage(bytes);
286
287            Ndef ndef = (Ndef) tag.getTechnology(adapter, TagTechnology.NDEF);
288            if (ndef != null) {
289                ndef.connect();
290                if (!ndef.isWritable()) {
291                    Toast.makeText(this, "Tag is read-only, not writing", Toast.LENGTH_SHORT)
292                            .show();
293                    return false;
294                }
295                ndef.writeNdefMessage(msg);
296                Toast.makeText(this, "Wrote message to pre-formatted tag", Toast.LENGTH_SHORT)
297                        .show();
298                return true;
299            } else {
300                NdefFormatable format = (NdefFormatable) tag.getTechnology(adapter,
301                        TagTechnology.NDEF_FORMATABLE);
302                if (format != null) {
303                    format.connect();
304                    format.format(msg);
305                    Toast.makeText(this, "Formatted tag and wrote message", Toast.LENGTH_SHORT)
306                            .show();
307                    return true;
308                }
309            }
310        } catch (Exception e) {
311            Log.e(TAG, "Failed to write tag", e);
312        }
313
314        Toast.makeText(this, "Failed to write tag", Toast.LENGTH_SHORT)
315                .show();
316
317        return false;
318    }
319
320    void buildTagViews(NdefMessage[] msgs) {
321        if (msgs == null || msgs.length == 0) {
322            return;
323        }
324
325        LayoutInflater inflater = LayoutInflater.from(this);
326        LinearLayout content = mTagContent;
327
328        // Clear out any old views in the content area, for example if you scan two tags in a row.
329        content.removeAllViews();
330
331        // Parse the first message in the list
332        //TODO figure out what to do when/if we support multiple messages per tag
333        ParsedNdefMessage parsedMsg = NdefMessageParser.parse(msgs[0]);
334
335        // Build views for all of the sub records
336        List<ParsedNdefRecord> records = parsedMsg.getRecords();
337        final int size = records.size();
338
339        for (int i = 0 ; i < size ; i++) {
340            ParsedNdefRecord record = records.get(i);
341            content.addView(record.getView(this, inflater, content, i));
342            inflater.inflate(R.layout.tag_divider, content, true);
343        }
344    }
345
346    @Override
347    public void onNewIntent(Intent intent) {
348        setIntent(intent);
349        resolveIntent(intent);
350    }
351
352    @Override
353    public void setTitle(CharSequence title) {
354        mTitle.setText(title);
355    }
356
357    @Override
358    public void onClick(View view) {
359        if (view == mDeleteButton) {
360            if (mTagUri == null) {
361                finish();
362            } else {
363                // The tag came from the database, start a service to delete it
364                TagService.delete(this, mTagUri);
365                finish();
366            }
367            Toast.makeText(this, getResources().getString(R.string.tag_deleted), Toast.LENGTH_SHORT)
368                    .show();
369        } else if (view == mDoneButton) {
370            finish();
371        } else if (view == mStar) {
372            if (mTagUri != null) {
373                TagService.setStar(this, mTagUri, mStar.isChecked());
374            }
375        }
376    }
377
378    interface ViewTagQuery {
379        final static String[] PROJECTION = new String[] {
380                NdefMessages.BYTES, // 0
381                NdefMessages.STARRED, // 1
382                NdefMessages.DATE, // 2
383        };
384
385        static final int COLUMN_BYTES = 0;
386        static final int COLUMN_STARRED = 1;
387        static final int COLUMN_DATE = 2;
388    }
389
390    /**
391     * Loads a tag from the database, parses it, and builds the views
392     */
393    final class LoadTagTask extends AsyncTask<Uri, Void, Cursor> {
394        @Override
395        public Cursor doInBackground(Uri... args) {
396            Cursor cursor = getContentResolver().query(args[0], ViewTagQuery.PROJECTION,
397                    null, null, null);
398
399            // Ensure the cursor loads its window
400            if (cursor != null) cursor.getCount();
401            return cursor;
402        }
403
404        @Override
405        public void onPostExecute(Cursor cursor) {
406            NdefMessage msg = null;
407            try {
408                if (cursor != null && cursor.moveToFirst()) {
409                    msg = new NdefMessage(cursor.getBlob(ViewTagQuery.COLUMN_BYTES));
410                    if (msg != null) {
411                        mDate.setText(DateUtils.getRelativeTimeSpanString(TagViewer.this,
412                                cursor.getLong(ViewTagQuery.COLUMN_DATE)));
413                        mStar.setChecked(cursor.getInt(ViewTagQuery.COLUMN_STARRED) != 0);
414                        mStar.setEnabled(true);
415                        buildTagViews(new NdefMessage[] { msg });
416                    }
417                }
418            } catch (FormatException e) {
419                Log.e(TAG, "invalid tag format", e);
420            } finally {
421                if (cursor != null) cursor.close();
422            }
423        }
424    }
425}
426