/* * Copyright (C) 2011 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.nfc; import com.android.nfc.RegisteredComponentCache.ComponentInfo; import android.app.Activity; import android.app.ActivityManagerNative; import android.app.IActivityManager; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; 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.os.RemoteException; import android.util.Log; import java.nio.charset.Charsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Dispatch of NFC events to start activities */ public class NfcDispatcher { private static final boolean DBG = NfcService.DBG; private static final String TAG = NfcService.TAG; private final Context mContext; private final IActivityManager mIActivityManager; private final RegisteredComponentCache mTechListFilters; private PackageManager mPackageManager; // Locked on this private PendingIntent mOverrideIntent; private IntentFilter[] mOverrideFilters; private String[][] mOverrideTechLists; public NfcDispatcher(Context context, P2pLinkManager p2pManager) { mContext = context; mIActivityManager = ActivityManagerNative.getDefault(); mTechListFilters = new RegisteredComponentCache(mContext, NfcAdapter.ACTION_TECH_DISCOVERED, NfcAdapter.ACTION_TECH_DISCOVERED); mPackageManager = context.getPackageManager(); } public synchronized void setForegroundDispatch(PendingIntent intent, IntentFilter[] filters, String[][] techLists) { if (DBG) Log.d(TAG, "Set Foreground Dispatch"); mOverrideIntent = intent; mOverrideFilters = filters; mOverrideTechLists = techLists; } /** Returns false if no activities were found to dispatch to */ public boolean dispatchTag(Tag tag, NdefMessage[] msgs) { if (DBG) { Log.d(TAG, "Dispatching tag"); Log.d(TAG, tag.toString()); } IntentFilter[] overrideFilters; PendingIntent overrideIntent; String[][] overrideTechLists; synchronized (this) { overrideFilters = mOverrideFilters; overrideIntent = mOverrideIntent; overrideTechLists = mOverrideTechLists; } // First look for dispatch overrides if (overrideIntent != null) { if (DBG) Log.d(TAG, "Attempting to dispatch tag with override"); try { if (dispatchTagInternal(tag, msgs, overrideIntent, overrideFilters, overrideTechLists)) { if (DBG) Log.d(TAG, "Dispatched to override"); return true; } Log.w(TAG, "Dispatch override registered, but no filters matched"); } catch (CanceledException e) { Log.w(TAG, "Dispatch overrides pending intent was canceled"); synchronized (this) { mOverrideFilters = null; mOverrideIntent = null; mOverrideTechLists = null; } } } // Try normal dispatch. try { return dispatchTagInternal(tag, msgs, null, null, null); } catch (CanceledException e) { Log.e(TAG, "CanceledException unexpected here", e); return false; } } private Intent buildTagIntent(Tag tag, NdefMessage[] msgs, String action) { Intent intent = new Intent(action); intent.putExtra(NfcAdapter.EXTRA_TAG, tag); intent.putExtra(NfcAdapter.EXTRA_ID, tag.getId()); intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, msgs); return intent; } /** This method places the launched activity in a (single) NFC * root task. We use NfcRootActivity as the root of the task, * which launches the passed-in intent as soon as it's created. */ private boolean startRootActivity(Intent intent) { Intent rootIntent = new Intent(mContext, NfcRootActivity.class); rootIntent.putExtra(NfcRootActivity.EXTRA_LAUNCH_INTENT, intent); rootIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); // Ideally we'd have used startActivityForResult() to determine whether the // NfcRootActivity was able to launch the intent, but startActivityForResult() // is not available on Context. Instead, we query the PackageManager beforehand // to determine if there is an Activity to handle this intent, and base the // result of off that. List<ResolveInfo> activities = mPackageManager.queryIntentActivities(intent, 0); // Try to start the activity regardless of the result. mContext.startActivity(rootIntent); if (activities.size() > 0) { return true; } else { return false; } } // Dispatch to either an override pending intent or a standard startActivity() private boolean dispatchTagInternal(Tag tag, NdefMessage[] msgs, PendingIntent overrideIntent, IntentFilter[] overrideFilters, String[][] overrideTechLists) throws CanceledException{ Intent intent; // // Try the NDEF content specific dispatch // if (msgs != null && msgs.length > 0) { NdefMessage msg = msgs[0]; NdefRecord[] records = msg.getRecords(); if (records.length > 0) { // Found valid NDEF data, try to dispatch that first NdefRecord record = records[0]; intent = buildTagIntent(tag, msgs, NfcAdapter.ACTION_NDEF_DISCOVERED); if (setTypeOrDataFromNdef(intent, record)) { // The record contains filterable data, try to start a matching activity if (startDispatchActivity(intent, overrideIntent, overrideFilters, overrideTechLists, records)) { // If an activity is found then skip further dispatching return true; } else { if (DBG) Log.d(TAG, "No activities for NDEF handling of " + intent); } } } } // // Try the technology specific dispatch // String[] tagTechs = tag.getTechList(); Arrays.sort(tagTechs); if (overrideIntent != null) { // There are dispatch overrides in place if (overrideTechLists != null) { for (String[] filterTechs : overrideTechLists) { if (filterMatch(tagTechs, filterTechs)) { // An override matched, send it to the foreground activity. intent = buildTagIntent(tag, msgs, NfcAdapter.ACTION_TECH_DISCOVERED); overrideIntent.send(mContext, Activity.RESULT_OK, intent); return true; } } } } else { // Standard tech dispatch path ArrayList<ResolveInfo> matches = new ArrayList<ResolveInfo>(); ArrayList<ComponentInfo> registered = mTechListFilters.getComponents(); // Check each registered activity to see if it matches for (ComponentInfo info : registered) { // Don't allow wild card matching if (filterMatch(tagTechs, info.techs) && isComponentEnabled(mPackageManager, info.resolveInfo)) { // Add the activity as a match if it's not already in the list if (!matches.contains(info.resolveInfo)) { matches.add(info.resolveInfo); } } } if (matches.size() == 1) { // Single match, launch directly intent = buildTagIntent(tag, msgs, NfcAdapter.ACTION_TECH_DISCOVERED); ResolveInfo info = matches.get(0); intent.setClassName(info.activityInfo.packageName, info.activityInfo.name); if (startRootActivity(intent)) { return true; } } else if (matches.size() > 1) { // Multiple matches, show a custom activity chooser dialog intent = new Intent(mContext, TechListChooserActivity.class); intent.putExtra(Intent.EXTRA_INTENT, buildTagIntent(tag, msgs, NfcAdapter.ACTION_TECH_DISCOVERED)); intent.putParcelableArrayListExtra(TechListChooserActivity.EXTRA_RESOLVE_INFOS, matches); if (startRootActivity(intent)) { return true; } } else { // No matches, move on if (DBG) Log.w(TAG, "No activities for technology handling"); } } // // Try the generic intent // intent = buildTagIntent(tag, msgs, NfcAdapter.ACTION_TAG_DISCOVERED); if (startDispatchActivity(intent, overrideIntent, overrideFilters, overrideTechLists, null)) { return true; } else { Log.e(TAG, "No tag fallback activity found for " + intent); return false; } } /* Starts the package main activity if it's already installed, or takes you to its * market page if not. * returns whether an activity was started. */ private boolean startActivityOrMarket(String packageName) { Intent intent = mPackageManager.getLaunchIntentForPackage(packageName); if (intent != null) { return (startRootActivity(intent)); } else { // Find the package in Market: Intent market = getAppSearchIntent(packageName); return(startRootActivity(market)); } } private boolean startDispatchActivity(Intent intent, PendingIntent overrideIntent, IntentFilter[] overrideFilters, String[][] overrideTechLists, NdefRecord[] records) throws CanceledException { if (overrideIntent != null) { boolean found = false; if (overrideFilters == null && overrideTechLists == null) { // No filters means to always dispatch regardless of match found = true; } else if (overrideFilters != null) { for (IntentFilter filter : overrideFilters) { if (filter.match(mContext.getContentResolver(), intent, false, TAG) >= 0) { found = true; break; } } } if (found) { Log.i(TAG, "Dispatching to override intent " + overrideIntent); overrideIntent.send(mContext, Activity.RESULT_OK, intent); return true; } else { return false; } } else { resumeAppSwitches(); if (records != null) { String firstPackage = null; for (NdefRecord record : records) { if (record.getTnf() == NdefRecord.TNF_EXTERNAL_TYPE) { if (Arrays.equals(record.getType(), NdefRecord.RTD_ANDROID_APP)) { String pkg = new String(record.getPayload(), Charsets.US_ASCII); if (firstPackage == null) { firstPackage = pkg; } intent.setPackage(pkg); if (startRootActivity(intent)) { return true; } } } } if (firstPackage != null) { // Found an Android package, but could not handle ndef intent. // If the application is installed, call its main activity, // or otherwise go to Market. if (startActivityOrMarket(firstPackage)) { return true; } } } return(startRootActivity(intent)); } } /** * Tells the ActivityManager to resume allowing app switches. * * If the current app called stopAppSwitches() then our startActivity() can * be delayed for several seconds. This happens with the default home * screen. As a system service we can override this behavior with * resumeAppSwitches(). */ void resumeAppSwitches() { try { mIActivityManager.resumeAppSwitches(); } catch (RemoteException e) { } } /** Returns true if the tech list filter matches the techs on the tag */ private boolean filterMatch(String[] tagTechs, String[] filterTechs) { if (filterTechs == null || filterTechs.length == 0) return false; for (String tech : filterTechs) { if (Arrays.binarySearch(tagTechs, tech) < 0) { return false; } } return true; } private boolean setTypeOrDataFromNdef(Intent intent, NdefRecord record) { short tnf = record.getTnf(); byte[] type = record.getType(); try { switch (tnf) { case NdefRecord.TNF_MIME_MEDIA: { intent.setType(new String(type, Charsets.US_ASCII)); return true; } case NdefRecord.TNF_ABSOLUTE_URI: { intent.setData(Uri.parse(new String(type, Charsets.UTF_8))); return true; } case NdefRecord.TNF_WELL_KNOWN: { byte[] payload = record.getPayload(); if (payload == null || payload.length == 0) return false; if (Arrays.equals(type, NdefRecord.RTD_TEXT)) { intent.setType("text/plain"); return true; } else if (Arrays.equals(type, NdefRecord.RTD_SMART_POSTER)) { // Parse the smart poster looking for the URI try { NdefMessage msg = new NdefMessage(record.getPayload()); for (NdefRecord subRecord : msg.getRecords()) { short subTnf = subRecord.getTnf(); if (subTnf == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(subRecord.getType(), NdefRecord.RTD_URI)) { intent.setData(NdefRecord.parseWellKnownUriRecord(subRecord)); return true; } else if (subTnf == NdefRecord.TNF_ABSOLUTE_URI) { intent.setData(Uri.parse(new String(subRecord.getType(), Charsets.UTF_8))); return true; } } } catch (FormatException e) { return false; } } else if (Arrays.equals(type, NdefRecord.RTD_URI)) { intent.setData(NdefRecord.parseWellKnownUriRecord(record)); return true; } return false; } case NdefRecord.TNF_EXTERNAL_TYPE: { intent.setData(Uri.parse("vnd.android.nfc://ext/" + new String(record.getType(), Charsets.US_ASCII))); return true; } } return false; } catch (Exception e) { Log.e(TAG, "failed to parse record", e); return false; } } /** * Returns an intent that can be used to find an application not currently * installed on the device. */ private static Intent getAppSearchIntent(String pkg) { Intent market = new Intent(Intent.ACTION_VIEW); market.setData(Uri.parse("market://details?id=" + pkg)); return market; } private static boolean isComponentEnabled(PackageManager pm, ResolveInfo info) { boolean enabled = false; ComponentName compname = new ComponentName( info.activityInfo.packageName, info.activityInfo.name); try { // Note that getActivityInfo() will internally call // isEnabledLP() to determine whether the component // enabled. If it's not, null is returned. if (pm.getActivityInfo(compname,0) != null) { enabled = true; } } catch (PackageManager.NameNotFoundException e) { enabled = false; } if (!enabled) { Log.d(TAG, "Component not enabled: " + compname); } return enabled; } }