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