Home | History | Annotate | Download | only in nfc
      1 /*
      2  * Copyright (C) 2011 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 
     17 package com.android.nfc;
     18 
     19 import android.bluetooth.BluetoothAdapter;
     20 
     21 import com.android.nfc.RegisteredComponentCache.ComponentInfo;
     22 import com.android.nfc.handover.HandoverDataParser;
     23 import com.android.nfc.handover.PeripheralHandoverService;
     24 
     25 import android.app.Activity;
     26 import android.app.ActivityManager;
     27 import android.app.ActivityManagerNative;
     28 import android.app.IActivityManager;
     29 import android.app.PendingIntent;
     30 import android.app.PendingIntent.CanceledException;
     31 import android.content.ComponentName;
     32 import android.content.ContentResolver;
     33 import android.content.Context;
     34 import android.content.Intent;
     35 import android.content.IntentFilter;
     36 import android.content.pm.PackageManager;
     37 import android.content.pm.PackageManager.NameNotFoundException;
     38 import android.content.pm.ResolveInfo;
     39 import android.content.res.Resources.NotFoundException;
     40 import android.net.Uri;
     41 import android.nfc.NdefMessage;
     42 import android.nfc.NdefRecord;
     43 import android.nfc.NfcAdapter;
     44 import android.nfc.Tag;
     45 import android.nfc.tech.Ndef;
     46 import android.os.RemoteException;
     47 import android.os.UserHandle;
     48 import android.util.Log;
     49 
     50 import java.io.FileDescriptor;
     51 import java.io.PrintWriter;
     52 import java.nio.charset.StandardCharsets;
     53 import java.util.ArrayList;
     54 import java.util.Arrays;
     55 import java.util.LinkedList;
     56 import java.util.List;
     57 
     58 /**
     59  * Dispatch of NFC events to start activities
     60  */
     61 class NfcDispatcher {
     62     private static final boolean DBG = false;
     63     private static final String TAG = "NfcDispatcher";
     64 
     65     static final int DISPATCH_SUCCESS = 1;
     66     static final int DISPATCH_FAIL = 2;
     67     static final int DISPATCH_UNLOCK = 3;
     68 
     69     private final Context mContext;
     70     private final IActivityManager mIActivityManager;
     71     private final RegisteredComponentCache mTechListFilters;
     72     private final ContentResolver mContentResolver;
     73     private final HandoverDataParser mHandoverDataParser;
     74     private final String[] mProvisioningMimes;
     75     private final ScreenStateHelper mScreenStateHelper;
     76     private final NfcUnlockManager mNfcUnlockManager;
     77     private final boolean mDeviceSupportsBluetooth;
     78 
     79     // Locked on this
     80     private PendingIntent mOverrideIntent;
     81     private IntentFilter[] mOverrideFilters;
     82     private String[][] mOverrideTechLists;
     83     private boolean mProvisioningOnly;
     84 
     85     NfcDispatcher(Context context,
     86                   HandoverDataParser handoverDataParser,
     87                   boolean provisionOnly) {
     88         mContext = context;
     89         mIActivityManager = ActivityManagerNative.getDefault();
     90         mTechListFilters = new RegisteredComponentCache(mContext,
     91                 NfcAdapter.ACTION_TECH_DISCOVERED, NfcAdapter.ACTION_TECH_DISCOVERED);
     92         mContentResolver = context.getContentResolver();
     93         mHandoverDataParser = handoverDataParser;
     94         mScreenStateHelper = new ScreenStateHelper(context);
     95         mNfcUnlockManager = NfcUnlockManager.getInstance();
     96         mDeviceSupportsBluetooth = BluetoothAdapter.getDefaultAdapter() != null;
     97 
     98         synchronized (this) {
     99             mProvisioningOnly = provisionOnly;
    100         }
    101         String[] provisionMimes = null;
    102         if (provisionOnly) {
    103             try {
    104                 // Get accepted mime-types
    105                 provisionMimes = context.getResources().
    106                         getStringArray(R.array.provisioning_mime_types);
    107             } catch (NotFoundException e) {
    108                provisionMimes = null;
    109             }
    110         }
    111         mProvisioningMimes = provisionMimes;
    112     }
    113 
    114     public synchronized void setForegroundDispatch(PendingIntent intent,
    115             IntentFilter[] filters, String[][] techLists) {
    116         if (DBG) Log.d(TAG, "Set Foreground Dispatch");
    117         mOverrideIntent = intent;
    118         mOverrideFilters = filters;
    119         mOverrideTechLists = techLists;
    120     }
    121 
    122     public synchronized void disableProvisioningMode() {
    123        mProvisioningOnly = false;
    124     }
    125 
    126     /**
    127      * Helper for re-used objects and methods during a single tag dispatch.
    128      */
    129     static class DispatchInfo {
    130         public final Intent intent;
    131 
    132         final Intent rootIntent;
    133         final Uri ndefUri;
    134         final String ndefMimeType;
    135         final PackageManager packageManager;
    136         final Context context;
    137 
    138         public DispatchInfo(Context context, Tag tag, NdefMessage message) {
    139             intent = new Intent();
    140             intent.putExtra(NfcAdapter.EXTRA_TAG, tag);
    141             intent.putExtra(NfcAdapter.EXTRA_ID, tag.getId());
    142             if (message != null) {
    143                 intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, new NdefMessage[] {message});
    144                 ndefUri = message.getRecords()[0].toUri();
    145                 ndefMimeType = message.getRecords()[0].toMimeType();
    146             } else {
    147                 ndefUri = null;
    148                 ndefMimeType = null;
    149             }
    150 
    151             rootIntent = new Intent(context, NfcRootActivity.class);
    152             rootIntent.putExtra(NfcRootActivity.EXTRA_LAUNCH_INTENT, intent);
    153             rootIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
    154 
    155             this.context = context;
    156             packageManager = context.getPackageManager();
    157         }
    158 
    159         public Intent setNdefIntent() {
    160             intent.setAction(NfcAdapter.ACTION_NDEF_DISCOVERED);
    161             if (ndefUri != null) {
    162                 intent.setData(ndefUri);
    163                 return intent;
    164             } else if (ndefMimeType != null) {
    165                 intent.setType(ndefMimeType);
    166                 return intent;
    167             }
    168             return null;
    169         }
    170 
    171         public Intent setTechIntent() {
    172             intent.setData(null);
    173             intent.setType(null);
    174             intent.setAction(NfcAdapter.ACTION_TECH_DISCOVERED);
    175             return intent;
    176         }
    177 
    178         public Intent setTagIntent() {
    179             intent.setData(null);
    180             intent.setType(null);
    181             intent.setAction(NfcAdapter.ACTION_TAG_DISCOVERED);
    182             return intent;
    183         }
    184 
    185         /**
    186          * Launch the activity via a (single) NFC root task, so that it
    187          * creates a new task stack instead of interfering with any existing
    188          * task stack for that activity.
    189          * NfcRootActivity acts as the task root, it immediately calls
    190          * start activity on the intent it is passed.
    191          */
    192         boolean tryStartActivity() {
    193             // Ideally we'd have used startActivityForResult() to determine whether the
    194             // NfcRootActivity was able to launch the intent, but startActivityForResult()
    195             // is not available on Context. Instead, we query the PackageManager beforehand
    196             // to determine if there is an Activity to handle this intent, and base the
    197             // result of off that.
    198             List<ResolveInfo> activities = packageManager.queryIntentActivitiesAsUser(intent, 0,
    199                     ActivityManager.getCurrentUser());
    200             if (activities.size() > 0) {
    201                 context.startActivityAsUser(rootIntent, UserHandle.CURRENT);
    202                 return true;
    203             }
    204             return false;
    205         }
    206 
    207         boolean tryStartActivity(Intent intentToStart) {
    208             List<ResolveInfo> activities = packageManager.queryIntentActivitiesAsUser(
    209                     intentToStart, 0, ActivityManager.getCurrentUser());
    210             if (activities.size() > 0) {
    211                 rootIntent.putExtra(NfcRootActivity.EXTRA_LAUNCH_INTENT, intentToStart);
    212                 context.startActivityAsUser(rootIntent, UserHandle.CURRENT);
    213                 return true;
    214             }
    215             return false;
    216         }
    217     }
    218 
    219     /** Returns:
    220      * <ul>
    221      *  <li /> DISPATCH_SUCCESS if dispatched to an activity,
    222      *  <li /> DISPATCH_FAIL if no activities were found to dispatch to,
    223      *  <li /> DISPATCH_UNLOCK if the tag was used to unlock the device
    224      * </ul>
    225      */
    226     public int dispatchTag(Tag tag) {
    227         PendingIntent overrideIntent;
    228         IntentFilter[] overrideFilters;
    229         String[][] overrideTechLists;
    230         boolean provisioningOnly;
    231 
    232         synchronized (this) {
    233             overrideFilters = mOverrideFilters;
    234             overrideIntent = mOverrideIntent;
    235             overrideTechLists = mOverrideTechLists;
    236             provisioningOnly = mProvisioningOnly;
    237         }
    238 
    239         boolean screenUnlocked = false;
    240         if (!provisioningOnly &&
    241                 mScreenStateHelper.checkScreenState() == ScreenStateHelper.SCREEN_STATE_ON_LOCKED) {
    242             screenUnlocked = handleNfcUnlock(tag);
    243             if (!screenUnlocked) {
    244                 return DISPATCH_FAIL;
    245             }
    246         }
    247 
    248         NdefMessage message = null;
    249         Ndef ndef = Ndef.get(tag);
    250         if (ndef != null) {
    251             message = ndef.getCachedNdefMessage();
    252         }
    253 
    254         if (DBG) Log.d(TAG, "dispatch tag: " + tag.toString() + " message: " + message);
    255 
    256         DispatchInfo dispatch = new DispatchInfo(mContext, tag, message);
    257 
    258         resumeAppSwitches();
    259 
    260         if (tryOverrides(dispatch, tag, message, overrideIntent, overrideFilters,
    261                 overrideTechLists)) {
    262             return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS;
    263         }
    264 
    265         if (tryPeripheralHandover(message)) {
    266             if (DBG) Log.i(TAG, "matched BT HANDOVER");
    267             return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS;
    268         }
    269 
    270         if (NfcWifiProtectedSetup.tryNfcWifiSetup(ndef, mContext)) {
    271             if (DBG) Log.i(TAG, "matched NFC WPS TOKEN");
    272             return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS;
    273         }
    274 
    275         if (tryNdef(dispatch, message, provisioningOnly)) {
    276             return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS;
    277         }
    278 
    279         if (screenUnlocked) {
    280             // We only allow NDEF-based mimeType matching in case of an unlock
    281             return DISPATCH_UNLOCK;
    282         }
    283 
    284         if (provisioningOnly) {
    285             // We only allow NDEF-based mimeType matching
    286             return DISPATCH_FAIL;
    287         }
    288 
    289         // Only allow NDEF-based mimeType matching for unlock tags
    290         if (tryTech(dispatch, tag)) {
    291             return DISPATCH_SUCCESS;
    292         }
    293 
    294         dispatch.setTagIntent();
    295         if (dispatch.tryStartActivity()) {
    296             if (DBG) Log.i(TAG, "matched TAG");
    297             return DISPATCH_SUCCESS;
    298         }
    299 
    300         if (DBG) Log.i(TAG, "no match");
    301         return DISPATCH_FAIL;
    302     }
    303 
    304     private boolean handleNfcUnlock(Tag tag) {
    305         return mNfcUnlockManager.tryUnlock(tag);
    306     }
    307 
    308     boolean tryOverrides(DispatchInfo dispatch, Tag tag, NdefMessage message, PendingIntent overrideIntent,
    309             IntentFilter[] overrideFilters, String[][] overrideTechLists) {
    310         if (overrideIntent == null) {
    311             return false;
    312         }
    313         Intent intent;
    314 
    315         // NDEF
    316         if (message != null) {
    317             intent = dispatch.setNdefIntent();
    318             if (intent != null &&
    319                     isFilterMatch(intent, overrideFilters, overrideTechLists != null)) {
    320                 try {
    321                     overrideIntent.send(mContext, Activity.RESULT_OK, intent);
    322                     if (DBG) Log.i(TAG, "matched NDEF override");
    323                     return true;
    324                 } catch (CanceledException e) {
    325                     return false;
    326                 }
    327             }
    328         }
    329 
    330         // TECH
    331         intent = dispatch.setTechIntent();
    332         if (isTechMatch(tag, overrideTechLists)) {
    333             try {
    334                 overrideIntent.send(mContext, Activity.RESULT_OK, intent);
    335                 if (DBG) Log.i(TAG, "matched TECH override");
    336                 return true;
    337             } catch (CanceledException e) {
    338                 return false;
    339             }
    340         }
    341 
    342         // TAG
    343         intent = dispatch.setTagIntent();
    344         if (isFilterMatch(intent, overrideFilters, overrideTechLists != null)) {
    345             try {
    346                 overrideIntent.send(mContext, Activity.RESULT_OK, intent);
    347                 if (DBG) Log.i(TAG, "matched TAG override");
    348                 return true;
    349             } catch (CanceledException e) {
    350                 return false;
    351             }
    352         }
    353         return false;
    354     }
    355 
    356     boolean isFilterMatch(Intent intent, IntentFilter[] filters, boolean hasTechFilter) {
    357         if (filters != null) {
    358             for (IntentFilter filter : filters) {
    359                 if (filter.match(mContentResolver, intent, false, TAG) >= 0) {
    360                     return true;
    361                 }
    362             }
    363         } else if (!hasTechFilter) {
    364             return true;  // always match if both filters and techlists are null
    365         }
    366         return false;
    367     }
    368 
    369     boolean isTechMatch(Tag tag, String[][] techLists) {
    370         if (techLists == null) {
    371             return false;
    372         }
    373 
    374         String[] tagTechs = tag.getTechList();
    375         Arrays.sort(tagTechs);
    376         for (String[] filterTechs : techLists) {
    377             if (filterMatch(tagTechs, filterTechs)) {
    378                 return true;
    379             }
    380         }
    381         return false;
    382     }
    383 
    384     boolean tryNdef(DispatchInfo dispatch, NdefMessage message, boolean provisioningOnly) {
    385         if (message == null) {
    386             return false;
    387         }
    388         Intent intent = dispatch.setNdefIntent();
    389 
    390         // Bail out if the intent does not contain filterable NDEF data
    391         if (intent == null) return false;
    392 
    393         if (provisioningOnly) {
    394             if (mProvisioningMimes == null ||
    395                     !(Arrays.asList(mProvisioningMimes).contains(intent.getType()))) {
    396                 Log.e(TAG, "Dropping NFC intent in provisioning mode.");
    397                 return false;
    398             }
    399         }
    400 
    401         // Try to start AAR activity with matching filter
    402         List<String> aarPackages = extractAarPackages(message);
    403         for (String pkg : aarPackages) {
    404             dispatch.intent.setPackage(pkg);
    405             if (dispatch.tryStartActivity()) {
    406                 if (DBG) Log.i(TAG, "matched AAR to NDEF");
    407                 return true;
    408             }
    409         }
    410 
    411         // Try to perform regular launch of the first AAR
    412         if (aarPackages.size() > 0) {
    413             String firstPackage = aarPackages.get(0);
    414             PackageManager pm;
    415             try {
    416                 UserHandle currentUser = new UserHandle(ActivityManager.getCurrentUser());
    417                 pm = mContext.createPackageContextAsUser("android", 0,
    418                         currentUser).getPackageManager();
    419             } catch (NameNotFoundException e) {
    420                 Log.e(TAG, "Could not create user package context");
    421                 return false;
    422             }
    423             Intent appLaunchIntent = pm.getLaunchIntentForPackage(firstPackage);
    424             if (appLaunchIntent != null && dispatch.tryStartActivity(appLaunchIntent)) {
    425                 if (DBG) Log.i(TAG, "matched AAR to application launch");
    426                 return true;
    427             }
    428             // Find the package in Market:
    429             Intent marketIntent = getAppSearchIntent(firstPackage);
    430             if (marketIntent != null && dispatch.tryStartActivity(marketIntent)) {
    431                 if (DBG) Log.i(TAG, "matched AAR to market launch");
    432                 return true;
    433             }
    434         }
    435 
    436         // regular launch
    437         dispatch.intent.setPackage(null);
    438         if (dispatch.tryStartActivity()) {
    439             if (DBG) Log.i(TAG, "matched NDEF");
    440             return true;
    441         }
    442 
    443         return false;
    444     }
    445 
    446     static List<String> extractAarPackages(NdefMessage message) {
    447         List<String> aarPackages = new LinkedList<String>();
    448         for (NdefRecord record : message.getRecords()) {
    449             String pkg = checkForAar(record);
    450             if (pkg != null) {
    451                 aarPackages.add(pkg);
    452             }
    453         }
    454         return aarPackages;
    455     }
    456 
    457     boolean tryTech(DispatchInfo dispatch, Tag tag) {
    458         dispatch.setTechIntent();
    459 
    460         String[] tagTechs = tag.getTechList();
    461         Arrays.sort(tagTechs);
    462 
    463         // Standard tech dispatch path
    464         ArrayList<ResolveInfo> matches = new ArrayList<ResolveInfo>();
    465         List<ComponentInfo> registered = mTechListFilters.getComponents();
    466 
    467         PackageManager pm;
    468         try {
    469             UserHandle currentUser = new UserHandle(ActivityManager.getCurrentUser());
    470             pm = mContext.createPackageContextAsUser("android", 0,
    471                     currentUser).getPackageManager();
    472         } catch (NameNotFoundException e) {
    473             Log.e(TAG, "Could not create user package context");
    474             return false;
    475         }
    476         // Check each registered activity to see if it matches
    477         for (ComponentInfo info : registered) {
    478             // Don't allow wild card matching
    479             if (filterMatch(tagTechs, info.techs) &&
    480                     isComponentEnabled(pm, info.resolveInfo)) {
    481                 // Add the activity as a match if it's not already in the list
    482                 if (!matches.contains(info.resolveInfo)) {
    483                     matches.add(info.resolveInfo);
    484                 }
    485             }
    486         }
    487 
    488         if (matches.size() == 1) {
    489             // Single match, launch directly
    490             ResolveInfo info = matches.get(0);
    491             dispatch.intent.setClassName(info.activityInfo.packageName, info.activityInfo.name);
    492             if (dispatch.tryStartActivity()) {
    493                 if (DBG) Log.i(TAG, "matched single TECH");
    494                 return true;
    495             }
    496             dispatch.intent.setComponent(null);
    497         } else if (matches.size() > 1) {
    498             // Multiple matches, show a custom activity chooser dialog
    499             Intent intent = new Intent(mContext, TechListChooserActivity.class);
    500             intent.putExtra(Intent.EXTRA_INTENT, dispatch.intent);
    501             intent.putParcelableArrayListExtra(TechListChooserActivity.EXTRA_RESOLVE_INFOS,
    502                     matches);
    503             if (dispatch.tryStartActivity(intent)) {
    504                 if (DBG) Log.i(TAG, "matched multiple TECH");
    505                 return true;
    506             }
    507         }
    508         return false;
    509     }
    510 
    511     public boolean tryPeripheralHandover(NdefMessage m) {
    512         if (m == null || !mDeviceSupportsBluetooth) return false;
    513 
    514         if (DBG) Log.d(TAG, "tryHandover(): " + m.toString());
    515 
    516         HandoverDataParser.BluetoothHandoverData handover = mHandoverDataParser.parseBluetooth(m);
    517         if (handover == null || !handover.valid) return false;
    518 
    519         Intent intent = new Intent(mContext, PeripheralHandoverService.class);
    520         intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_DEVICE, handover.device);
    521         intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_NAME, handover.name);
    522         intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_TRANSPORT, handover.transport);
    523         mContext.startServiceAsUser(intent, UserHandle.CURRENT);
    524 
    525         return true;
    526     }
    527 
    528 
    529     /**
    530      * Tells the ActivityManager to resume allowing app switches.
    531      *
    532      * If the current app called stopAppSwitches() then our startActivity() can
    533      * be delayed for several seconds. This happens with the default home
    534      * screen.  As a system service we can override this behavior with
    535      * resumeAppSwitches().
    536     */
    537     void resumeAppSwitches() {
    538         try {
    539             mIActivityManager.resumeAppSwitches();
    540         } catch (RemoteException e) { }
    541     }
    542 
    543     /** Returns true if the tech list filter matches the techs on the tag */
    544     boolean filterMatch(String[] tagTechs, String[] filterTechs) {
    545         if (filterTechs == null || filterTechs.length == 0) return false;
    546 
    547         for (String tech : filterTechs) {
    548             if (Arrays.binarySearch(tagTechs, tech) < 0) {
    549                 return false;
    550             }
    551         }
    552         return true;
    553     }
    554 
    555     static String checkForAar(NdefRecord record) {
    556         if (record.getTnf() == NdefRecord.TNF_EXTERNAL_TYPE &&
    557                 Arrays.equals(record.getType(), NdefRecord.RTD_ANDROID_APP)) {
    558             return new String(record.getPayload(), StandardCharsets.US_ASCII);
    559         }
    560         return null;
    561     }
    562 
    563     /**
    564      * Returns an intent that can be used to find an application not currently
    565      * installed on the device.
    566      */
    567     static Intent getAppSearchIntent(String pkg) {
    568         Intent market = new Intent(Intent.ACTION_VIEW);
    569         market.setData(Uri.parse("market://details?id=" + pkg));
    570         return market;
    571     }
    572 
    573     static boolean isComponentEnabled(PackageManager pm, ResolveInfo info) {
    574         boolean enabled = false;
    575         ComponentName compname = new ComponentName(
    576                 info.activityInfo.packageName, info.activityInfo.name);
    577         try {
    578             // Note that getActivityInfo() will internally call
    579             // isEnabledLP() to determine whether the component
    580             // enabled. If it's not, null is returned.
    581             if (pm.getActivityInfo(compname,0) != null) {
    582                 enabled = true;
    583             }
    584         } catch (PackageManager.NameNotFoundException e) {
    585             enabled = false;
    586         }
    587         if (!enabled) {
    588             Log.d(TAG, "Component not enabled: " + compname);
    589         }
    590         return enabled;
    591     }
    592 
    593     void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    594         synchronized (this) {
    595             pw.println("mOverrideIntent=" + mOverrideIntent);
    596             pw.println("mOverrideFilters=" + mOverrideFilters);
    597             pw.println("mOverrideTechLists=" + mOverrideTechLists);
    598         }
    599     }
    600 }
    601