| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.apps.tag; |
| |
| import com.android.apps.tag.message.NdefMessageParser; |
| import com.android.apps.tag.message.ParsedNdefMessage; |
| import com.android.apps.tag.provider.TagContract.NdefMessages; |
| import com.android.apps.tag.record.ParsedNdefRecord; |
| |
| import android.app.Activity; |
| import android.app.PendingIntent; |
| import android.content.BroadcastReceiver; |
| import android.content.ContentUris; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.SharedPreferences; |
| import android.content.res.AssetFileDescriptor; |
| import android.database.Cursor; |
| import android.media.AudioManager; |
| import android.media.MediaPlayer; |
| import android.net.Uri; |
| import android.nfc.FormatException; |
| import android.nfc.NdefMessage; |
| import android.nfc.NdefRecord; |
| import android.nfc.NfcAdapter; |
| import android.nfc.Tag; |
| import android.nfc.tech.Ndef; |
| import android.nfc.tech.NdefFormatable; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.os.Parcelable; |
| import android.os.PowerManager; |
| import android.os.PowerManager.WakeLock; |
| import android.text.format.DateUtils; |
| import android.util.Log; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.View.OnClickListener; |
| import android.view.WindowManager; |
| import android.widget.Button; |
| import android.widget.CheckBox; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import java.io.IOException; |
| import java.util.List; |
| |
| /** |
| * An {@link Activity} which handles a broadcast of a new tag that the device just discovered. |
| */ |
| public class TagViewer extends Activity implements OnClickListener { |
| static final String TAG = "SaveTag"; |
| static final String EXTRA_TAG_DB_ID = "db_id"; |
| static final String EXTRA_MESSAGE = "msg"; |
| static final String EXTRA_KEEP_TITLE = "keepTitle"; |
| |
| static final boolean SHOW_OVER_LOCK_SCREEN = false; |
| |
| /** This activity will finish itself in this amount of time if the user doesn't do anything. */ |
| static final int ACTIVITY_TIMEOUT_MS = 7 * 1000; |
| |
| Uri mTagUri; |
| ImageView mIcon; |
| TextView mTitle; |
| TextView mDate; |
| CheckBox mStar; |
| Button mDeleteButton; |
| Button mDoneButton; |
| LinearLayout mTagContent; |
| |
| BroadcastReceiver mReceiver; |
| |
| private class ScreenOffReceiver extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) { |
| if (!isFinishing()) { |
| finish(); |
| } |
| } |
| } |
| } |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| if (SHOW_OVER_LOCK_SCREEN) { |
| getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); |
| } |
| |
| setContentView(R.layout.tag_viewer); |
| |
| mTagContent = (LinearLayout) findViewById(R.id.list); |
| mTitle = (TextView) findViewById(R.id.title); |
| mDate = (TextView) findViewById(R.id.date); |
| mIcon = (ImageView) findViewById(R.id.icon); |
| mStar = (CheckBox) findViewById(R.id.star); |
| mDeleteButton = (Button) findViewById(R.id.button_delete); |
| mDoneButton = (Button) findViewById(R.id.button_done); |
| |
| mDeleteButton.setOnClickListener(this); |
| mDoneButton.setOnClickListener(this); |
| mStar.setOnClickListener(this); |
| mIcon.setImageResource(R.drawable.ic_launcher_nfc); |
| |
| resolveIntent(getIntent()); |
| } |
| |
| @Override |
| public void onRestart() { |
| super.onRestart(); |
| if (mTagUri == null) { |
| // Someone how the user was fast enough to navigate away from the activity |
| // before the service was able to save the tag and call back onto this |
| // activity with the pending intent. Since we don't know what do display here |
| // just finish the activity. |
| finish(); |
| } |
| } |
| |
| @Override |
| public void onStop() { |
| super.onStop(); |
| |
| PendingIntent pending = getPendingIntent(); |
| pending.cancel(); |
| |
| if (mReceiver != null) { |
| unregisterReceiver(mReceiver); |
| mReceiver = null; |
| } |
| } |
| |
| private PendingIntent getPendingIntent() { |
| Intent callback = new Intent(); |
| callback.setClass(this, TagViewer.class); |
| callback.setAction(Intent.ACTION_VIEW); |
| callback.setFlags(Intent. FLAG_ACTIVITY_CLEAR_TOP); |
| callback.putExtra(EXTRA_KEEP_TITLE, true); |
| |
| return PendingIntent.getActivity(this, 0, callback, PendingIntent.FLAG_CANCEL_CURRENT); |
| } |
| |
| void resolveIntent(Intent intent) { |
| // Parse the intent |
| String action = intent.getAction(); |
| if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action) |
| || NfcAdapter.ACTION_TECHNOLOGY_DISCOVERED.equals(action)) { |
| if (SHOW_OVER_LOCK_SCREEN) { |
| // A tag was just scanned so poke the user activity wake lock to keep |
| // the screen on a bit longer in the event that the activity has |
| // hidden the lock screen. |
| PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); |
| WakeLock wakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, TAG); |
| // This lock CANNOT be manually released in onStop() since that may |
| // cause a lock under run exception to be thrown when the timeout |
| // hits. |
| wakeLock.acquire(ACTIVITY_TIMEOUT_MS); |
| |
| if (mReceiver == null) { |
| mReceiver = new ScreenOffReceiver(); |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(Intent.ACTION_SCREEN_OFF); |
| registerReceiver(mReceiver, filter); |
| } |
| } |
| |
| // When a tag is discovered we send it to the service to be save. We |
| // include a PendingIntent for the service to call back onto. This |
| // will cause this activity to be restarted with onNewIntent(). At |
| // that time we read it from the database and view it. |
| Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); |
| NdefMessage[] msgs; |
| if (rawMsgs != null && rawMsgs.length > 0) { |
| // stupid java, need to cast one-by-one |
| msgs = new NdefMessage[rawMsgs.length]; |
| for (int i=0; i<rawMsgs.length; i++) { |
| msgs[i] = (NdefMessage) rawMsgs[i]; |
| } |
| } else { |
| // Unknown tag type |
| byte[] empty = new byte[] {}; |
| NdefRecord record = new NdefRecord(NdefRecord.TNF_UNKNOWN, empty, empty, empty); |
| NdefMessage msg = new NdefMessage(new NdefRecord[] { record }); |
| msgs = new NdefMessage[] { msg }; |
| } |
| TagService.saveMessages(this, msgs, false, getPendingIntent()); |
| |
| // Setup the views |
| setTitle(R.string.title_scanned_tag); |
| mDate.setVisibility(View.GONE); |
| mStar.setChecked(false); |
| mStar.setEnabled(true); |
| |
| // Play notification. |
| try { |
| AssetFileDescriptor afd = getResources().openRawResourceFd( |
| R.raw.discovered_tag_notification); |
| if (afd != null) { |
| MediaPlayer player = new MediaPlayer(); |
| player.setDataSource( |
| afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); |
| afd.close(); |
| player.setAudioStreamType(AudioManager.STREAM_NOTIFICATION); |
| player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { |
| @Override |
| public void onCompletion(MediaPlayer mp) { |
| mp.release(); |
| } |
| }); |
| player.prepare(); |
| player.start(); |
| } |
| } catch (IOException ex) { |
| Log.d(TAG, "Unable to play sound for tag discovery", ex); |
| } catch (IllegalArgumentException ex) { |
| Log.d(TAG, "Unable to play sound for tag discovery", ex); |
| } catch (SecurityException ex) { |
| Log.d(TAG, "Unable to play sound for tag discovery", ex); |
| } |
| |
| } else if (Intent.ACTION_VIEW.equals(action)) { |
| // Setup the views |
| if (!intent.getBooleanExtra(EXTRA_KEEP_TITLE, false)) { |
| setTitle(R.string.title_existing_tag); |
| mDate.setVisibility(View.VISIBLE); |
| } |
| |
| mStar.setVisibility(View.VISIBLE); |
| mStar.setEnabled(false); // it's reenabled when the async load completes |
| |
| // Read the tag from the database asynchronously |
| mTagUri = intent.getData(); |
| new LoadTagTask().execute(mTagUri); |
| } else { |
| Log.e(TAG, "Unknown intent " + intent); |
| finish(); |
| return; |
| } |
| } |
| |
| void buildTagViews(NdefMessage[] msgs) { |
| if (msgs == null || msgs.length == 0) { |
| return; |
| } |
| |
| LayoutInflater inflater = LayoutInflater.from(this); |
| LinearLayout content = mTagContent; |
| |
| // Clear out any old views in the content area, for example if you scan two tags in a row. |
| content.removeAllViews(); |
| |
| // Parse the first message in the list |
| //TODO figure out what to do when/if we support multiple messages per tag |
| ParsedNdefMessage parsedMsg = NdefMessageParser.parse(msgs[0]); |
| |
| // Build views for all of the sub records |
| List<ParsedNdefRecord> records = parsedMsg.getRecords(); |
| final int size = records.size(); |
| |
| for (int i = 0 ; i < size ; i++) { |
| ParsedNdefRecord record = records.get(i); |
| content.addView(record.getView(this, inflater, content, i)); |
| inflater.inflate(R.layout.tag_divider, content, true); |
| } |
| } |
| |
| @Override |
| public void onNewIntent(Intent intent) { |
| setIntent(intent); |
| resolveIntent(intent); |
| } |
| |
| @Override |
| public void setTitle(CharSequence title) { |
| mTitle.setText(title); |
| } |
| |
| @Override |
| public void onClick(View view) { |
| if (view == mDeleteButton) { |
| if (mTagUri == null) { |
| finish(); |
| } else { |
| // The tag came from the database, start a service to delete it |
| TagService.delete(this, mTagUri); |
| finish(); |
| } |
| Toast.makeText(this, getResources().getString(R.string.tag_deleted), Toast.LENGTH_SHORT) |
| .show(); |
| } else if (view == mDoneButton) { |
| finish(); |
| } else if (view == mStar) { |
| if (mTagUri != null) { |
| TagService.setStar(this, mTagUri, mStar.isChecked()); |
| } |
| } |
| } |
| |
| interface ViewTagQuery { |
| final static String[] PROJECTION = new String[] { |
| NdefMessages.BYTES, // 0 |
| NdefMessages.STARRED, // 1 |
| NdefMessages.DATE, // 2 |
| }; |
| |
| static final int COLUMN_BYTES = 0; |
| static final int COLUMN_STARRED = 1; |
| static final int COLUMN_DATE = 2; |
| } |
| |
| /** |
| * Loads a tag from the database, parses it, and builds the views |
| */ |
| final class LoadTagTask extends AsyncTask<Uri, Void, Cursor> { |
| @Override |
| public Cursor doInBackground(Uri... args) { |
| Cursor cursor = getContentResolver().query(args[0], ViewTagQuery.PROJECTION, |
| null, null, null); |
| |
| // Ensure the cursor loads its window |
| if (cursor != null) cursor.getCount(); |
| return cursor; |
| } |
| |
| @Override |
| public void onPostExecute(Cursor cursor) { |
| NdefMessage msg = null; |
| try { |
| if (cursor != null && cursor.moveToFirst()) { |
| msg = new NdefMessage(cursor.getBlob(ViewTagQuery.COLUMN_BYTES)); |
| if (msg != null) { |
| mDate.setText(DateUtils.getRelativeTimeSpanString(TagViewer.this, |
| cursor.getLong(ViewTagQuery.COLUMN_DATE))); |
| mStar.setChecked(cursor.getInt(ViewTagQuery.COLUMN_STARRED) != 0); |
| mStar.setEnabled(true); |
| buildTagViews(new NdefMessage[] { msg }); |
| } |
| } |
| } catch (FormatException e) { |
| Log.e(TAG, "invalid tag format", e); |
| } finally { |
| if (cursor != null) cursor.close(); |
| } |
| } |
| } |
| } |