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