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