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