| /* |
| * 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 com.android.nfc.handover.HandoverManager; |
| |
| import android.app.Activity; |
| import android.app.ActivityManager; |
| import android.app.ActivityManagerNative; |
| import android.app.IActivityManager; |
| import android.app.PendingIntent; |
| import android.app.PendingIntent.CanceledException; |
| import android.content.ComponentName; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManager.NameNotFoundException; |
| import android.content.pm.ResolveInfo; |
| import android.net.Uri; |
| import android.nfc.NdefMessage; |
| import android.nfc.NdefRecord; |
| import android.nfc.NfcAdapter; |
| import android.nfc.Tag; |
| import android.nfc.tech.Ndef; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.util.Log; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.nio.charset.Charsets; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.LinkedList; |
| import java.util.List; |
| |
| /** |
| * Dispatch of NFC events to start activities |
| */ |
| public class NfcDispatcher { |
| static final boolean DBG = true; |
| static final String TAG = "NfcDispatcher"; |
| |
| final Context mContext; |
| final IActivityManager mIActivityManager; |
| final RegisteredComponentCache mTechListFilters; |
| final ContentResolver mContentResolver; |
| final HandoverManager mHandoverManager; |
| |
| // Locked on this |
| PendingIntent mOverrideIntent; |
| IntentFilter[] mOverrideFilters; |
| String[][] mOverrideTechLists; |
| |
| public NfcDispatcher(Context context, HandoverManager handoverManager) { |
| mContext = context; |
| mIActivityManager = ActivityManagerNative.getDefault(); |
| mTechListFilters = new RegisteredComponentCache(mContext, |
| NfcAdapter.ACTION_TECH_DISCOVERED, NfcAdapter.ACTION_TECH_DISCOVERED); |
| mContentResolver = context.getContentResolver(); |
| mHandoverManager = handoverManager; |
| } |
| |
| public synchronized void setForegroundDispatch(PendingIntent intent, |
| IntentFilter[] filters, String[][] techLists) { |
| if (DBG) Log.d(TAG, "Set Foreground Dispatch"); |
| mOverrideIntent = intent; |
| mOverrideFilters = filters; |
| mOverrideTechLists = techLists; |
| } |
| |
| /** |
| * Helper for re-used objects and methods during a single tag dispatch. |
| */ |
| static class DispatchInfo { |
| public final Intent intent; |
| |
| final Intent rootIntent; |
| final Uri ndefUri; |
| final String ndefMimeType; |
| final PackageManager packageManager; |
| final Context context; |
| |
| public DispatchInfo(Context context, Tag tag, NdefMessage message) { |
| intent = new Intent(); |
| intent.putExtra(NfcAdapter.EXTRA_TAG, tag); |
| intent.putExtra(NfcAdapter.EXTRA_ID, tag.getId()); |
| if (message != null) { |
| intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, new NdefMessage[] {message}); |
| ndefUri = message.getRecords()[0].toUri(); |
| ndefMimeType = message.getRecords()[0].toMimeType(); |
| } else { |
| ndefUri = null; |
| ndefMimeType = null; |
| } |
| |
| rootIntent = new Intent(context, NfcRootActivity.class); |
| rootIntent.putExtra(NfcRootActivity.EXTRA_LAUNCH_INTENT, intent); |
| rootIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); |
| |
| this.context = context; |
| packageManager = context.getPackageManager(); |
| } |
| |
| public Intent setNdefIntent() { |
| intent.setAction(NfcAdapter.ACTION_NDEF_DISCOVERED); |
| if (ndefUri != null) { |
| intent.setData(ndefUri); |
| return intent; |
| } else if (ndefMimeType != null) { |
| intent.setType(ndefMimeType); |
| return intent; |
| } |
| return null; |
| } |
| |
| public Intent setTechIntent() { |
| intent.setData(null); |
| intent.setType(null); |
| intent.setAction(NfcAdapter.ACTION_TECH_DISCOVERED); |
| return intent; |
| } |
| |
| public Intent setTagIntent() { |
| intent.setData(null); |
| intent.setType(null); |
| intent.setAction(NfcAdapter.ACTION_TAG_DISCOVERED); |
| return intent; |
| } |
| |
| /** |
| * Launch the activity via a (single) NFC root task, so that it |
| * creates a new task stack instead of interfering with any existing |
| * task stack for that activity. |
| * NfcRootActivity acts as the task root, it immediately calls |
| * start activity on the intent it is passed. |
| */ |
| boolean tryStartActivity() { |
| // 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 = packageManager.queryIntentActivitiesAsUser(intent, 0, |
| ActivityManager.getCurrentUser()); |
| if (activities.size() > 0) { |
| context.startActivityAsUser(rootIntent, UserHandle.CURRENT); |
| return true; |
| } |
| return false; |
| } |
| |
| boolean tryStartActivity(Intent intentToStart) { |
| List<ResolveInfo> activities = packageManager.queryIntentActivitiesAsUser( |
| intentToStart, 0, ActivityManager.getCurrentUser()); |
| if (activities.size() > 0) { |
| rootIntent.putExtra(NfcRootActivity.EXTRA_LAUNCH_INTENT, intentToStart); |
| context.startActivityAsUser(rootIntent, UserHandle.CURRENT); |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| /** Returns false if no activities were found to dispatch to */ |
| public boolean dispatchTag(Tag tag) { |
| NdefMessage message = null; |
| Ndef ndef = Ndef.get(tag); |
| if (ndef != null) { |
| message = ndef.getCachedNdefMessage(); |
| } |
| if (DBG) Log.d(TAG, "dispatch tag: " + tag.toString() + " message: " + message); |
| |
| PendingIntent overrideIntent; |
| IntentFilter[] overrideFilters; |
| String[][] overrideTechLists; |
| |
| DispatchInfo dispatch = new DispatchInfo(mContext, tag, message); |
| synchronized (this) { |
| overrideFilters = mOverrideFilters; |
| overrideIntent = mOverrideIntent; |
| overrideTechLists = mOverrideTechLists; |
| } |
| |
| resumeAppSwitches(); |
| |
| if (tryOverrides(dispatch, tag, message, overrideIntent, overrideFilters, overrideTechLists)) { |
| return true; |
| } |
| |
| if (mHandoverManager.tryHandover(message)) { |
| if (DBG) Log.i(TAG, "matched BT HANDOVER"); |
| return true; |
| } |
| |
| if (tryNdef(dispatch, message)) { |
| return true; |
| } |
| |
| if (tryTech(dispatch, tag)) { |
| return true; |
| } |
| |
| dispatch.setTagIntent(); |
| if (dispatch.tryStartActivity()) { |
| if (DBG) Log.i(TAG, "matched TAG"); |
| return true; |
| } |
| |
| if (DBG) Log.i(TAG, "no match"); |
| return false; |
| } |
| |
| boolean tryOverrides(DispatchInfo dispatch, Tag tag, NdefMessage message, PendingIntent overrideIntent, |
| IntentFilter[] overrideFilters, String[][] overrideTechLists) { |
| if (overrideIntent == null) { |
| return false; |
| } |
| Intent intent; |
| |
| // NDEF |
| if (message != null) { |
| intent = dispatch.setNdefIntent(); |
| if (intent != null && |
| isFilterMatch(intent, overrideFilters, overrideTechLists != null)) { |
| try { |
| overrideIntent.send(mContext, Activity.RESULT_OK, intent); |
| if (DBG) Log.i(TAG, "matched NDEF override"); |
| return true; |
| } catch (CanceledException e) { |
| return false; |
| } |
| } |
| } |
| |
| // TECH |
| intent = dispatch.setTechIntent(); |
| if (isTechMatch(tag, overrideTechLists)) { |
| try { |
| overrideIntent.send(mContext, Activity.RESULT_OK, intent); |
| if (DBG) Log.i(TAG, "matched TECH override"); |
| return true; |
| } catch (CanceledException e) { |
| return false; |
| } |
| } |
| |
| // TAG |
| intent = dispatch.setTagIntent(); |
| if (isFilterMatch(intent, overrideFilters, overrideTechLists != null)) { |
| try { |
| overrideIntent.send(mContext, Activity.RESULT_OK, intent); |
| if (DBG) Log.i(TAG, "matched TAG override"); |
| return true; |
| } catch (CanceledException e) { |
| return false; |
| } |
| } |
| return false; |
| } |
| |
| boolean isFilterMatch(Intent intent, IntentFilter[] filters, boolean hasTechFilter) { |
| if (filters != null) { |
| for (IntentFilter filter : filters) { |
| if (filter.match(mContentResolver, intent, false, TAG) >= 0) { |
| return true; |
| } |
| } |
| } else if (!hasTechFilter) { |
| return true; // always match if both filters and techlists are null |
| } |
| return false; |
| } |
| |
| boolean isTechMatch(Tag tag, String[][] techLists) { |
| if (techLists == null) { |
| return false; |
| } |
| |
| String[] tagTechs = tag.getTechList(); |
| Arrays.sort(tagTechs); |
| for (String[] filterTechs : techLists) { |
| if (filterMatch(tagTechs, filterTechs)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| boolean tryNdef(DispatchInfo dispatch, NdefMessage message) { |
| if (message == null) { |
| return false; |
| } |
| Intent intent = dispatch.setNdefIntent(); |
| |
| // Bail out if the intent does not contain filterable NDEF data |
| if (intent == null) return false; |
| |
| // Try to start AAR activity with matching filter |
| List<String> aarPackages = extractAarPackages(message); |
| for (String pkg : aarPackages) { |
| dispatch.intent.setPackage(pkg); |
| if (dispatch.tryStartActivity()) { |
| if (DBG) Log.i(TAG, "matched AAR to NDEF"); |
| return true; |
| } |
| } |
| |
| // Try to perform regular launch of the first AAR |
| if (aarPackages.size() > 0) { |
| String firstPackage = aarPackages.get(0); |
| PackageManager pm; |
| try { |
| UserHandle currentUser = new UserHandle(ActivityManager.getCurrentUser()); |
| pm = mContext.createPackageContextAsUser("android", 0, |
| currentUser).getPackageManager(); |
| } catch (NameNotFoundException e) { |
| Log.e(TAG, "Could not create user package context"); |
| return false; |
| } |
| Intent appLaunchIntent = pm.getLaunchIntentForPackage(firstPackage); |
| if (appLaunchIntent != null && dispatch.tryStartActivity(appLaunchIntent)) { |
| if (DBG) Log.i(TAG, "matched AAR to application launch"); |
| return true; |
| } |
| // Find the package in Market: |
| Intent marketIntent = getAppSearchIntent(firstPackage); |
| if (marketIntent != null && dispatch.tryStartActivity(marketIntent)) { |
| if (DBG) Log.i(TAG, "matched AAR to market launch"); |
| return true; |
| } |
| } |
| |
| // regular launch |
| dispatch.intent.setPackage(null); |
| if (dispatch.tryStartActivity()) { |
| if (DBG) Log.i(TAG, "matched NDEF"); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| static List<String> extractAarPackages(NdefMessage message) { |
| List<String> aarPackages = new LinkedList<String>(); |
| for (NdefRecord record : message.getRecords()) { |
| String pkg = checkForAar(record); |
| if (pkg != null) { |
| aarPackages.add(pkg); |
| } |
| } |
| return aarPackages; |
| } |
| |
| boolean tryTech(DispatchInfo dispatch, Tag tag) { |
| dispatch.setTechIntent(); |
| |
| String[] tagTechs = tag.getTechList(); |
| Arrays.sort(tagTechs); |
| |
| // Standard tech dispatch path |
| ArrayList<ResolveInfo> matches = new ArrayList<ResolveInfo>(); |
| List<ComponentInfo> registered = mTechListFilters.getComponents(); |
| |
| PackageManager pm; |
| try { |
| UserHandle currentUser = new UserHandle(ActivityManager.getCurrentUser()); |
| pm = mContext.createPackageContextAsUser("android", 0, |
| currentUser).getPackageManager(); |
| } catch (NameNotFoundException e) { |
| Log.e(TAG, "Could not create user package context"); |
| return false; |
| } |
| // 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(pm, 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 |
| ResolveInfo info = matches.get(0); |
| dispatch.intent.setClassName(info.activityInfo.packageName, info.activityInfo.name); |
| if (dispatch.tryStartActivity()) { |
| if (DBG) Log.i(TAG, "matched single TECH"); |
| return true; |
| } |
| dispatch.intent.setClassName((String)null, null); |
| } else if (matches.size() > 1) { |
| // Multiple matches, show a custom activity chooser dialog |
| Intent intent = new Intent(mContext, TechListChooserActivity.class); |
| intent.putExtra(Intent.EXTRA_INTENT, dispatch.intent); |
| intent.putParcelableArrayListExtra(TechListChooserActivity.EXTRA_RESOLVE_INFOS, |
| matches); |
| if (dispatch.tryStartActivity(intent)) { |
| if (DBG) Log.i(TAG, "matched multiple TECH"); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * 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 */ |
| 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; |
| } |
| |
| static String checkForAar(NdefRecord record) { |
| if (record.getTnf() == NdefRecord.TNF_EXTERNAL_TYPE && |
| Arrays.equals(record.getType(), NdefRecord.RTD_ANDROID_APP)) { |
| return new String(record.getPayload(), Charsets.US_ASCII); |
| } |
| return null; |
| } |
| |
| /** |
| * Returns an intent that can be used to find an application not currently |
| * installed on the device. |
| */ |
| static Intent getAppSearchIntent(String pkg) { |
| Intent market = new Intent(Intent.ACTION_VIEW); |
| market.setData(Uri.parse("market://details?id=" + pkg)); |
| return market; |
| } |
| |
| 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; |
| } |
| |
| void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| synchronized (this) { |
| pw.println("mOverrideIntent=" + mOverrideIntent); |
| pw.println("mOverrideFilters=" + mOverrideFilters); |
| pw.println("mOverrideTechLists=" + mOverrideTechLists); |
| } |
| } |
| } |