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