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.Manifest;
     20 import android.app.ActivityManager;
     21 import android.bluetooth.BluetoothAdapter;
     22 import android.os.UserManager;
     23 
     24 import com.android.nfc.RegisteredComponentCache.ComponentInfo;
     25 import com.android.nfc.handover.HandoverDataParser;
     26 import com.android.nfc.handover.PeripheralHandoverService;
     27 
     28 import android.app.Activity;
     29 import android.app.ActivityManager;
     30 import android.app.AlertDialog;
     31 import android.app.IActivityManager;
     32 import android.app.PendingIntent;
     33 import android.app.PendingIntent.CanceledException;
     34 import android.content.ComponentName;
     35 import android.content.ContentResolver;
     36 import android.content.Context;
     37 import android.content.DialogInterface;
     38 import android.content.Intent;
     39 import android.content.IntentFilter;
     40 import android.content.pm.PackageManager;
     41 import android.content.pm.PackageManager.NameNotFoundException;
     42 import android.content.pm.ResolveInfo;
     43 import android.content.res.Resources.NotFoundException;
     44 import android.net.Uri;
     45 import android.nfc.NdefMessage;
     46 import android.nfc.NdefRecord;
     47 import android.nfc.NfcAdapter;
     48 import android.nfc.Tag;
     49 import android.nfc.tech.Ndef;
     50 import android.nfc.tech.NfcBarcode;
     51 import android.os.RemoteException;
     52 import android.os.UserHandle;
     53 import android.util.Log;
     54 import android.view.LayoutInflater;
     55 import android.view.View;
     56 import android.view.WindowManager;
     57 import android.widget.TextView;
     58 
     59 import java.io.FileDescriptor;
     60 import java.io.PrintWriter;
     61 import java.nio.charset.StandardCharsets;
     62 import java.util.ArrayList;
     63 import java.util.Arrays;
     64 import java.util.LinkedList;
     65 import java.util.List;
     66 import java.util.Locale;
     67 
     68 /**
     69  * Dispatch of NFC events to start activities
     70  */
     71 class NfcDispatcher {
     72     private static final boolean DBG = false;
     73     private static final String TAG = "NfcDispatcher";
     74 
     75     static final int DISPATCH_SUCCESS = 1;
     76     static final int DISPATCH_FAIL = 2;
     77     static final int DISPATCH_UNLOCK = 3;
     78 
     79     private final Context mContext;
     80     private final IActivityManager mIActivityManager;
     81     private final RegisteredComponentCache mTechListFilters;
     82     private final ContentResolver mContentResolver;
     83     private final HandoverDataParser mHandoverDataParser;
     84     private final String[] mProvisioningMimes;
     85     private final String[] mLiveCaseMimes;
     86     private final ScreenStateHelper mScreenStateHelper;
     87     private final NfcUnlockManager mNfcUnlockManager;
     88     private final boolean mDeviceSupportsBluetooth;
     89 
     90     // Locked on this
     91     private PendingIntent mOverrideIntent;
     92     private IntentFilter[] mOverrideFilters;
     93     private String[][] mOverrideTechLists;
     94     private boolean mProvisioningOnly;
     95 
     96     NfcDispatcher(Context context,
     97                   HandoverDataParser handoverDataParser,
     98                   boolean provisionOnly,
     99                   boolean isLiveCaseEnabled) {
    100         mContext = context;
    101         mIActivityManager = ActivityManager.getService();
    102         mTechListFilters = new RegisteredComponentCache(mContext,
    103                 NfcAdapter.ACTION_TECH_DISCOVERED, NfcAdapter.ACTION_TECH_DISCOVERED);
    104         mContentResolver = context.getContentResolver();
    105         mHandoverDataParser = handoverDataParser;
    106         mScreenStateHelper = new ScreenStateHelper(context);
    107         mNfcUnlockManager = NfcUnlockManager.getInstance();
    108         mDeviceSupportsBluetooth = BluetoothAdapter.getDefaultAdapter() != null;
    109 
    110         synchronized (this) {
    111             mProvisioningOnly = provisionOnly;
    112         }
    113         String[] provisionMimes = null;
    114         if (provisionOnly) {
    115             try {
    116                 // Get accepted mime-types
    117                 provisionMimes = context.getResources().
    118                         getStringArray(R.array.provisioning_mime_types);
    119             } catch (NotFoundException e) {
    120                provisionMimes = null;
    121             }
    122         }
    123         mProvisioningMimes = provisionMimes;
    124 
    125         String[] liveCaseMimes = null;
    126         if (isLiveCaseEnabled) {
    127             try {
    128                 // Get accepted mime-types
    129                 liveCaseMimes = context.getResources().
    130                         getStringArray(R.array.live_case_mime_types);
    131             } catch (NotFoundException e) {
    132                liveCaseMimes = null;
    133             }
    134         }
    135         mLiveCaseMimes = liveCaseMimes;
    136     }
    137 
    138     public synchronized void setForegroundDispatch(PendingIntent intent,
    139             IntentFilter[] filters, String[][] techLists) {
    140         if (DBG) Log.d(TAG, "Set Foreground Dispatch");
    141         mOverrideIntent = intent;
    142         mOverrideFilters = filters;
    143         mOverrideTechLists = techLists;
    144     }
    145 
    146     public synchronized void disableProvisioningMode() {
    147        mProvisioningOnly = false;
    148     }
    149 
    150     /**
    151      * Helper for re-used objects and methods during a single tag dispatch.
    152      */
    153     static class DispatchInfo {
    154         public final Intent intent;
    155 
    156         final Intent rootIntent;
    157         final Uri ndefUri;
    158         final String ndefMimeType;
    159         final PackageManager packageManager;
    160         final Context context;
    161 
    162         public DispatchInfo(Context context, Tag tag, NdefMessage message) {
    163             intent = new Intent();
    164             intent.putExtra(NfcAdapter.EXTRA_TAG, tag);
    165             intent.putExtra(NfcAdapter.EXTRA_ID, tag.getId());
    166             if (message != null) {
    167                 intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, new NdefMessage[] {message});
    168                 ndefUri = message.getRecords()[0].toUri();
    169                 ndefMimeType = message.getRecords()[0].toMimeType();
    170             } else {
    171                 ndefUri = null;
    172                 ndefMimeType = null;
    173             }
    174 
    175             rootIntent = new Intent(context, NfcRootActivity.class);
    176             rootIntent.putExtra(NfcRootActivity.EXTRA_LAUNCH_INTENT, intent);
    177             rootIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
    178 
    179             this.context = context;
    180             packageManager = context.getPackageManager();
    181         }
    182 
    183         public Intent setNdefIntent() {
    184             intent.setAction(NfcAdapter.ACTION_NDEF_DISCOVERED);
    185             if (ndefUri != null) {
    186                 intent.setData(ndefUri);
    187                 return intent;
    188             } else if (ndefMimeType != null) {
    189                 intent.setType(ndefMimeType);
    190                 return intent;
    191             }
    192             return null;
    193         }
    194 
    195         public Intent setTechIntent() {
    196             intent.setData(null);
    197             intent.setType(null);
    198             intent.setAction(NfcAdapter.ACTION_TECH_DISCOVERED);
    199             return intent;
    200         }
    201 
    202         public Intent setTagIntent() {
    203             intent.setData(null);
    204             intent.setType(null);
    205             intent.setAction(NfcAdapter.ACTION_TAG_DISCOVERED);
    206             return intent;
    207         }
    208 
    209         public boolean isWebIntent() {
    210             return ndefUri != null && ndefUri.normalizeScheme().getScheme().startsWith("http");
    211         }
    212 
    213         public String getUri() {
    214             return ndefUri.toString();
    215         }
    216 
    217         /**
    218          * Launch the activity via a (single) NFC root task, so that it
    219          * creates a new task stack instead of interfering with any existing
    220          * task stack for that activity.
    221          * NfcRootActivity acts as the task root, it immediately calls
    222          * start activity on the intent it is passed.
    223          */
    224         boolean tryStartActivity() {
    225             // Ideally we'd have used startActivityForResult() to determine whether the
    226             // NfcRootActivity was able to launch the intent, but startActivityForResult()
    227             // is not available on Context. Instead, we query the PackageManager beforehand
    228             // to determine if there is an Activity to handle this intent, and base the
    229             // result of off that.
    230             List<ResolveInfo> activities = packageManager.queryIntentActivitiesAsUser(intent, 0,
    231                     ActivityManager.getCurrentUser());
    232             if (activities.size() > 0) {
    233                 context.startActivityAsUser(rootIntent, UserHandle.CURRENT);
    234                 return true;
    235             }
    236             return false;
    237         }
    238 
    239         boolean tryStartActivity(Intent intentToStart) {
    240             List<ResolveInfo> activities = packageManager.queryIntentActivitiesAsUser(
    241                     intentToStart, 0, ActivityManager.getCurrentUser());
    242             if (activities.size() > 0) {
    243                 rootIntent.putExtra(NfcRootActivity.EXTRA_LAUNCH_INTENT, intentToStart);
    244                 context.startActivityAsUser(rootIntent, UserHandle.CURRENT);
    245                 return true;
    246             }
    247             return false;
    248         }
    249     }
    250 
    251     /** Returns:
    252      * <ul>
    253      *  <li /> DISPATCH_SUCCESS if dispatched to an activity,
    254      *  <li /> DISPATCH_FAIL if no activities were found to dispatch to,
    255      *  <li /> DISPATCH_UNLOCK if the tag was used to unlock the device
    256      * </ul>
    257      */
    258     public int dispatchTag(Tag tag) {
    259         PendingIntent overrideIntent;
    260         IntentFilter[] overrideFilters;
    261         String[][] overrideTechLists;
    262         String[] provisioningMimes;
    263         String[] liveCaseMimes;
    264         NdefMessage message = null;
    265         boolean provisioningOnly;
    266 
    267         synchronized (this) {
    268             overrideFilters = mOverrideFilters;
    269             overrideIntent = mOverrideIntent;
    270             overrideTechLists = mOverrideTechLists;
    271             provisioningOnly = mProvisioningOnly;
    272             provisioningMimes = mProvisioningMimes;
    273             liveCaseMimes = mLiveCaseMimes;
    274         }
    275 
    276         boolean screenUnlocked = false;
    277         boolean liveCaseDetected = false;
    278         Ndef ndef = Ndef.get(tag);
    279         if (!provisioningOnly &&
    280                 mScreenStateHelper.checkScreenState() == ScreenStateHelper.SCREEN_STATE_ON_LOCKED) {
    281             screenUnlocked = handleNfcUnlock(tag);
    282 
    283             if (ndef != null) {
    284                 message = ndef.getCachedNdefMessage();
    285                 if (message != null) {
    286                     String ndefMimeType = message.getRecords()[0].toMimeType();
    287                     if (liveCaseMimes != null &&
    288                             Arrays.asList(liveCaseMimes).contains(ndefMimeType)) {
    289                         liveCaseDetected = true;
    290                     }
    291                 }
    292             }
    293 
    294             if (!screenUnlocked && !liveCaseDetected)
    295                 return DISPATCH_FAIL;
    296         }
    297 
    298         if (ndef != null) {
    299             message = ndef.getCachedNdefMessage();
    300         } else {
    301             NfcBarcode nfcBarcode = NfcBarcode.get(tag);
    302             if (nfcBarcode != null && nfcBarcode.getType() == NfcBarcode.TYPE_KOVIO) {
    303                 message = decodeNfcBarcodeUri(nfcBarcode);
    304             }
    305         }
    306 
    307         if (DBG) Log.d(TAG, "dispatch tag: " + tag.toString() + " message: " + message);
    308 
    309         DispatchInfo dispatch = new DispatchInfo(mContext, tag, message);
    310 
    311         resumeAppSwitches();
    312 
    313         if (tryOverrides(dispatch, tag, message, overrideIntent, overrideFilters,
    314                 overrideTechLists)) {
    315             return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS;
    316         }
    317 
    318         if (tryPeripheralHandover(message)) {
    319             if (DBG) Log.i(TAG, "matched BT HANDOVER");
    320             return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS;
    321         }
    322 
    323         if (NfcWifiProtectedSetup.tryNfcWifiSetup(ndef, mContext)) {
    324             if (DBG) Log.i(TAG, "matched NFC WPS TOKEN");
    325             return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS;
    326         }
    327 
    328         if (provisioningOnly) {
    329             if (message == null) {
    330                 // We only allow NDEF-message dispatch in provisioning mode
    331                 return DISPATCH_FAIL;
    332             }
    333             // Restrict to mime-types in whitelist.
    334             String ndefMimeType = message.getRecords()[0].toMimeType();
    335             if (provisioningMimes == null ||
    336                     !(Arrays.asList(provisioningMimes).contains(ndefMimeType))) {
    337                 Log.e(TAG, "Dropping NFC intent in provisioning mode.");
    338                 return DISPATCH_FAIL;
    339             }
    340         }
    341 
    342         if (tryNdef(dispatch, message)) {
    343             return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS;
    344         }
    345 
    346         if (screenUnlocked) {
    347             // We only allow NDEF-based mimeType matching in case of an unlock
    348             return DISPATCH_UNLOCK;
    349         }
    350 
    351         // Only allow NDEF-based mimeType matching for unlock tags
    352         if (tryTech(dispatch, tag)) {
    353             return DISPATCH_SUCCESS;
    354         }
    355 
    356         dispatch.setTagIntent();
    357         if (dispatch.tryStartActivity()) {
    358             if (DBG) Log.i(TAG, "matched TAG");
    359             return DISPATCH_SUCCESS;
    360         }
    361 
    362         if (DBG) Log.i(TAG, "no match");
    363         return DISPATCH_FAIL;
    364     }
    365 
    366     private boolean handleNfcUnlock(Tag tag) {
    367         return mNfcUnlockManager.tryUnlock(tag);
    368     }
    369 
    370     /**
    371      * Checks for the presence of a URL stored in a tag with tech NfcBarcode.
    372      * If found, decodes URL and returns NdefMessage message containing an
    373      * NdefRecord containing the decoded URL. If not found, returns null.
    374      *
    375      * URLs are decoded as follows:
    376      *
    377      * Ignore first byte (which is 0x80 ORd with a manufacturer ID, corresponding
    378      * to ISO/IEC 7816-6).
    379      * The second byte describes the payload data format. There are four defined data
    380      * format values that identify URL data. Depending on the data format value, the
    381      * associated prefix is appended to the URL data:
    382      *
    383      * 0x01: URL with "http://www." prefix
    384      * 0x02: URL with "https://www." prefix
    385      * 0x03: URL with "http://" prefix
    386      * 0x04: URL with "https://" prefix
    387      *
    388      * Other data format values do not identify URL data and are not handled by this function.
    389      * URL payload is encoded in US-ASCII, following the limitations defined in RFC3987.
    390      * see http://www.ietf.org/rfc/rfc3987.txt
    391      *
    392      * The final two bytes of a tag with tech NfcBarcode are always reserved for CRC data,
    393      * and are therefore not part of the payload. They are ignored in the decoding of a URL.
    394      *
    395      * The default assumption is that the URL occupies the entire payload of the NfcBarcode
    396      * ID and all bytes of the NfcBarcode payload are decoded until the CRC (final two bytes)
    397      * is reached. However, the OPTIONAL early terminator byte 0xfe can be used to signal
    398      * an early end of the URL. Once this function reaches an early terminator byte 0xfe,
    399      * URL decoding stops and the NdefMessage is created and returned. Any payload data after
    400      * the first early terminator byte is ignored for the purposes of URL decoding.
    401      */
    402     private NdefMessage decodeNfcBarcodeUri(NfcBarcode nfcBarcode) {
    403         final byte URI_PREFIX_HTTP_WWW  = (byte) 0x01; // "http://www."
    404         final byte URI_PREFIX_HTTPS_WWW = (byte) 0x02; // "https://www."
    405         final byte URI_PREFIX_HTTP      = (byte) 0x03; // "http://"
    406         final byte URI_PREFIX_HTTPS     = (byte) 0x04; // "https://"
    407 
    408         NdefMessage message = null;
    409         byte[] tagId = nfcBarcode.getTag().getId();
    410         // All tags of NfcBarcode technology and Kovio type have lengths of a multiple of 16 bytes
    411         if (tagId.length >= 4
    412                 && (tagId[1] == URI_PREFIX_HTTP_WWW || tagId[1] == URI_PREFIX_HTTPS_WWW
    413                     || tagId[1] == URI_PREFIX_HTTP || tagId[1] == URI_PREFIX_HTTPS)) {
    414             // Look for optional URI terminator (0xfe), used to indicate the end of a URI prior to
    415             // the end of the full NfcBarcode payload. No terminator means that the URI occupies the
    416             // entire length of the payload field. Exclude checking the CRC in the final two bytes
    417             // of the NfcBarcode tagId.
    418             int end = 2;
    419             for (; end < tagId.length - 2; end++) {
    420                 if (tagId[end] == (byte) 0xfe) {
    421                     break;
    422                 }
    423             }
    424             byte[] payload = new byte[end - 1]; // Skip also first byte (manufacturer ID)
    425             System.arraycopy(tagId, 1, payload, 0, payload.length);
    426             NdefRecord uriRecord = new NdefRecord(
    427                     NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_URI, tagId, payload);
    428             message = new NdefMessage(uriRecord);
    429         }
    430         return message;
    431     }
    432 
    433     boolean tryOverrides(DispatchInfo dispatch, Tag tag, NdefMessage message, PendingIntent overrideIntent,
    434             IntentFilter[] overrideFilters, String[][] overrideTechLists) {
    435         if (overrideIntent == null) {
    436             return false;
    437         }
    438         Intent intent;
    439 
    440         // NDEF
    441         if (message != null) {
    442             intent = dispatch.setNdefIntent();
    443             if (intent != null &&
    444                     isFilterMatch(intent, overrideFilters, overrideTechLists != null)) {
    445                 try {
    446                     overrideIntent.send(mContext, Activity.RESULT_OK, intent);
    447                     if (DBG) Log.i(TAG, "matched NDEF override");
    448                     return true;
    449                 } catch (CanceledException e) {
    450                     return false;
    451                 }
    452             }
    453         }
    454 
    455         // TECH
    456         intent = dispatch.setTechIntent();
    457         if (isTechMatch(tag, overrideTechLists)) {
    458             try {
    459                 overrideIntent.send(mContext, Activity.RESULT_OK, intent);
    460                 if (DBG) Log.i(TAG, "matched TECH override");
    461                 return true;
    462             } catch (CanceledException e) {
    463                 return false;
    464             }
    465         }
    466 
    467         // TAG
    468         intent = dispatch.setTagIntent();
    469         if (isFilterMatch(intent, overrideFilters, overrideTechLists != null)) {
    470             try {
    471                 overrideIntent.send(mContext, Activity.RESULT_OK, intent);
    472                 if (DBG) Log.i(TAG, "matched TAG override");
    473                 return true;
    474             } catch (CanceledException e) {
    475                 return false;
    476             }
    477         }
    478         return false;
    479     }
    480 
    481     boolean isFilterMatch(Intent intent, IntentFilter[] filters, boolean hasTechFilter) {
    482         if (filters != null) {
    483             for (IntentFilter filter : filters) {
    484                 if (filter.match(mContentResolver, intent, false, TAG) >= 0) {
    485                     return true;
    486                 }
    487             }
    488         } else if (!hasTechFilter) {
    489             return true;  // always match if both filters and techlists are null
    490         }
    491         return false;
    492     }
    493 
    494     boolean isTechMatch(Tag tag, String[][] techLists) {
    495         if (techLists == null) {
    496             return false;
    497         }
    498 
    499         String[] tagTechs = tag.getTechList();
    500         Arrays.sort(tagTechs);
    501         for (String[] filterTechs : techLists) {
    502             if (filterMatch(tagTechs, filterTechs)) {
    503                 return true;
    504             }
    505         }
    506         return false;
    507     }
    508 
    509     boolean tryNdef(DispatchInfo dispatch, NdefMessage message) {
    510         if (message == null) {
    511             return false;
    512         }
    513         Intent intent = dispatch.setNdefIntent();
    514 
    515         // Bail out if the intent does not contain filterable NDEF data
    516         if (intent == null) return false;
    517 
    518         // Try to start AAR activity with matching filter
    519         List<String> aarPackages = extractAarPackages(message);
    520         for (String pkg : aarPackages) {
    521             dispatch.intent.setPackage(pkg);
    522             if (dispatch.tryStartActivity()) {
    523                 if (DBG) Log.i(TAG, "matched AAR to NDEF");
    524                 return true;
    525             }
    526         }
    527 
    528         // Try to perform regular launch of the first AAR
    529         if (aarPackages.size() > 0) {
    530             String firstPackage = aarPackages.get(0);
    531             PackageManager pm;
    532             try {
    533                 UserHandle currentUser = new UserHandle(ActivityManager.getCurrentUser());
    534                 pm = mContext.createPackageContextAsUser("android", 0,
    535                         currentUser).getPackageManager();
    536             } catch (NameNotFoundException e) {
    537                 Log.e(TAG, "Could not create user package context");
    538                 return false;
    539             }
    540             Intent appLaunchIntent = pm.getLaunchIntentForPackage(firstPackage);
    541             if (appLaunchIntent != null && dispatch.tryStartActivity(appLaunchIntent)) {
    542                 if (DBG) Log.i(TAG, "matched AAR to application launch");
    543                 return true;
    544             }
    545             // Find the package in Market:
    546             Intent marketIntent = getAppSearchIntent(firstPackage);
    547             if (marketIntent != null && dispatch.tryStartActivity(marketIntent)) {
    548                 if (DBG) Log.i(TAG, "matched AAR to market launch");
    549                 return true;
    550             }
    551         }
    552 
    553         // regular launch
    554         dispatch.intent.setPackage(null);
    555 
    556         if (dispatch.isWebIntent()) {
    557             if (DBG) Log.i(TAG, "matched Web link - prompting user");
    558             showWebLinkConfirmation(dispatch);
    559             return true;
    560         }
    561 
    562         if (dispatch.tryStartActivity()) {
    563             if (DBG) Log.i(TAG, "matched NDEF");
    564             return true;
    565         }
    566 
    567         return false;
    568     }
    569 
    570     static List<String> extractAarPackages(NdefMessage message) {
    571         List<String> aarPackages = new LinkedList<String>();
    572         for (NdefRecord record : message.getRecords()) {
    573             String pkg = checkForAar(record);
    574             if (pkg != null) {
    575                 aarPackages.add(pkg);
    576             }
    577         }
    578         return aarPackages;
    579     }
    580 
    581     boolean tryTech(DispatchInfo dispatch, Tag tag) {
    582         dispatch.setTechIntent();
    583 
    584         String[] tagTechs = tag.getTechList();
    585         Arrays.sort(tagTechs);
    586 
    587         // Standard tech dispatch path
    588         ArrayList<ResolveInfo> matches = new ArrayList<ResolveInfo>();
    589         List<ComponentInfo> registered = mTechListFilters.getComponents();
    590 
    591         PackageManager pm;
    592         try {
    593             UserHandle currentUser = new UserHandle(ActivityManager.getCurrentUser());
    594             pm = mContext.createPackageContextAsUser("android", 0,
    595                     currentUser).getPackageManager();
    596         } catch (NameNotFoundException e) {
    597             Log.e(TAG, "Could not create user package context");
    598             return false;
    599         }
    600         // Check each registered activity to see if it matches
    601         for (ComponentInfo info : registered) {
    602             // Don't allow wild card matching
    603             if (filterMatch(tagTechs, info.techs) &&
    604                     isComponentEnabled(pm, info.resolveInfo)) {
    605                 // Add the activity as a match if it's not already in the list
    606                 if (!matches.contains(info.resolveInfo)) {
    607                     matches.add(info.resolveInfo);
    608                 }
    609             }
    610         }
    611 
    612         if (matches.size() == 1) {
    613             // Single match, launch directly
    614             ResolveInfo info = matches.get(0);
    615             dispatch.intent.setClassName(info.activityInfo.packageName, info.activityInfo.name);
    616             if (dispatch.tryStartActivity()) {
    617                 if (DBG) Log.i(TAG, "matched single TECH");
    618                 return true;
    619             }
    620             dispatch.intent.setComponent(null);
    621         } else if (matches.size() > 1) {
    622             // Multiple matches, show a custom activity chooser dialog
    623             Intent intent = new Intent(mContext, TechListChooserActivity.class);
    624             intent.putExtra(Intent.EXTRA_INTENT, dispatch.intent);
    625             intent.putParcelableArrayListExtra(TechListChooserActivity.EXTRA_RESOLVE_INFOS,
    626                     matches);
    627             if (dispatch.tryStartActivity(intent)) {
    628                 if (DBG) Log.i(TAG, "matched multiple TECH");
    629                 return true;
    630             }
    631         }
    632         return false;
    633     }
    634 
    635     public boolean tryPeripheralHandover(NdefMessage m) {
    636         if (m == null || !mDeviceSupportsBluetooth) return false;
    637 
    638         if (DBG) Log.d(TAG, "tryHandover(): " + m.toString());
    639 
    640         HandoverDataParser.BluetoothHandoverData handover = mHandoverDataParser.parseBluetooth(m);
    641         if (handover == null || !handover.valid) return false;
    642         if (UserManager.get(mContext).hasUserRestriction(
    643                 UserManager.DISALLOW_CONFIG_BLUETOOTH,
    644                 // hasUserRestriction does not support UserHandle.CURRENT
    645                 UserHandle.of(ActivityManager.getCurrentUser()))) {
    646             return false;
    647         }
    648 
    649         Intent intent = new Intent(mContext, PeripheralHandoverService.class);
    650         intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_DEVICE, handover.device);
    651         intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_NAME, handover.name);
    652         intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_TRANSPORT, handover.transport);
    653         if (handover.oobData != null) {
    654             intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_OOB_DATA, handover.oobData);
    655         }
    656         if (handover.uuids != null) {
    657             intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_UUIDS, handover.uuids);
    658         }
    659         if (handover.btClass != null) {
    660             intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_CLASS, handover.btClass);
    661         }
    662         mContext.startServiceAsUser(intent, UserHandle.CURRENT);
    663 
    664         return true;
    665     }
    666 
    667 
    668     /**
    669      * Tells the ActivityManager to resume allowing app switches.
    670      *
    671      * If the current app called stopAppSwitches() then our startActivity() can
    672      * be delayed for several seconds. This happens with the default home
    673      * screen.  As a system service we can override this behavior with
    674      * resumeAppSwitches().
    675     */
    676     void resumeAppSwitches() {
    677         try {
    678             mIActivityManager.resumeAppSwitches();
    679         } catch (RemoteException e) { }
    680     }
    681 
    682     /** Returns true if the tech list filter matches the techs on the tag */
    683     boolean filterMatch(String[] tagTechs, String[] filterTechs) {
    684         if (filterTechs == null || filterTechs.length == 0) return false;
    685 
    686         for (String tech : filterTechs) {
    687             if (Arrays.binarySearch(tagTechs, tech) < 0) {
    688                 return false;
    689             }
    690         }
    691         return true;
    692     }
    693 
    694     static String checkForAar(NdefRecord record) {
    695         if (record.getTnf() == NdefRecord.TNF_EXTERNAL_TYPE &&
    696                 Arrays.equals(record.getType(), NdefRecord.RTD_ANDROID_APP)) {
    697             return new String(record.getPayload(), StandardCharsets.US_ASCII);
    698         }
    699         return null;
    700     }
    701 
    702     /**
    703      * Returns an intent that can be used to find an application not currently
    704      * installed on the device.
    705      */
    706     static Intent getAppSearchIntent(String pkg) {
    707         Intent market = new Intent(Intent.ACTION_VIEW);
    708         market.setData(Uri.parse("market://details?id=" + pkg));
    709         return market;
    710     }
    711 
    712     static boolean isComponentEnabled(PackageManager pm, ResolveInfo info) {
    713         boolean enabled = false;
    714         ComponentName compname = new ComponentName(
    715                 info.activityInfo.packageName, info.activityInfo.name);
    716         try {
    717             // Note that getActivityInfo() will internally call
    718             // isEnabledLP() to determine whether the component
    719             // enabled. If it's not, null is returned.
    720             if (pm.getActivityInfo(compname,0) != null) {
    721                 enabled = true;
    722             }
    723         } catch (PackageManager.NameNotFoundException e) {
    724             enabled = false;
    725         }
    726         if (!enabled) {
    727             Log.d(TAG, "Component not enabled: " + compname);
    728         }
    729         return enabled;
    730     }
    731 
    732     void showWebLinkConfirmation(DispatchInfo dispatch) {
    733         if (!mContext.getResources().getBoolean(R.bool.enable_nfc_url_open_dialog)) {
    734             dispatch.tryStartActivity();
    735             return;
    736         }
    737         AlertDialog.Builder builder = new AlertDialog.Builder(
    738                 mContext.getApplicationContext(),
    739                 android.R.style.Theme_DeviceDefault_Light_Dialog_Alert);
    740         builder.setTitle(R.string.title_confirm_url_open);
    741         LayoutInflater inflater = LayoutInflater.from(mContext);
    742         View view = inflater.inflate(R.layout.url_open_confirmation, null);
    743         if (view != null) {
    744             TextView url = view.findViewById(R.id.url_open_confirmation_link);
    745             if (url != null) {
    746                 url.setText(dispatch.getUri());
    747             }
    748             builder.setView(view);
    749         }
    750         builder.setNegativeButton(R.string.cancel, (dialog, which) -> {});
    751         builder.setPositiveButton(R.string.action_confirm_url_open, (dialog, which) -> {
    752             dispatch.tryStartActivity();
    753         });
    754         AlertDialog dialog = builder.create();
    755         dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
    756         dialog.show();
    757     }
    758 
    759     void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    760         synchronized (this) {
    761             pw.println("mOverrideIntent=" + mOverrideIntent);
    762             pw.println("mOverrideFilters=" + mOverrideFilters);
    763             pw.println("mOverrideTechLists=" + mOverrideTechLists);
    764         }
    765     }
    766 }
    767