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 
     21 import android.app.Activity;
     22 import android.app.ActivityManagerNative;
     23 import android.app.IActivityManager;
     24 import android.app.PendingIntent;
     25 import android.app.PendingIntent.CanceledException;
     26 import android.content.ComponentName;
     27 import android.content.Context;
     28 import android.content.Intent;
     29 import android.content.IntentFilter;
     30 import android.content.pm.PackageManager;
     31 import android.content.pm.ResolveInfo;
     32 import android.net.Uri;
     33 import android.nfc.FormatException;
     34 import android.nfc.NdefMessage;
     35 import android.nfc.NdefRecord;
     36 import android.nfc.NfcAdapter;
     37 import android.nfc.Tag;
     38 import android.os.RemoteException;
     39 import android.util.Log;
     40 
     41 import java.nio.charset.Charsets;
     42 import java.util.ArrayList;
     43 import java.util.Arrays;
     44 import java.util.List;
     45 
     46 /**
     47  * Dispatch of NFC events to start activities
     48  */
     49 public class NfcDispatcher {
     50     private static final boolean DBG = NfcService.DBG;
     51     private static final String TAG = NfcService.TAG;
     52 
     53     private final Context mContext;
     54     private final IActivityManager mIActivityManager;
     55     private final RegisteredComponentCache mTechListFilters;
     56 
     57     private PackageManager mPackageManager;
     58 
     59     // Locked on this
     60     private PendingIntent mOverrideIntent;
     61     private IntentFilter[] mOverrideFilters;
     62     private String[][] mOverrideTechLists;
     63 
     64     public NfcDispatcher(Context context, P2pLinkManager p2pManager) {
     65         mContext = context;
     66         mIActivityManager = ActivityManagerNative.getDefault();
     67         mTechListFilters = new RegisteredComponentCache(mContext,
     68                 NfcAdapter.ACTION_TECH_DISCOVERED, NfcAdapter.ACTION_TECH_DISCOVERED);
     69         mPackageManager = context.getPackageManager();
     70     }
     71 
     72     public synchronized void setForegroundDispatch(PendingIntent intent,
     73             IntentFilter[] filters, String[][] techLists) {
     74         if (DBG) Log.d(TAG, "Set Foreground Dispatch");
     75         mOverrideIntent = intent;
     76         mOverrideFilters = filters;
     77         mOverrideTechLists = techLists;
     78     }
     79 
     80     /** Returns false if no activities were found to dispatch to */
     81     public boolean dispatchTag(Tag tag, NdefMessage[] msgs) {
     82         if (DBG) {
     83             Log.d(TAG, "Dispatching tag");
     84             Log.d(TAG, tag.toString());
     85         }
     86 
     87         IntentFilter[] overrideFilters;
     88         PendingIntent overrideIntent;
     89         String[][] overrideTechLists;
     90         synchronized (this) {
     91             overrideFilters = mOverrideFilters;
     92             overrideIntent = mOverrideIntent;
     93             overrideTechLists = mOverrideTechLists;
     94         }
     95 
     96         // First look for dispatch overrides
     97         if (overrideIntent != null) {
     98             if (DBG) Log.d(TAG, "Attempting to dispatch tag with override");
     99             try {
    100                 if (dispatchTagInternal(tag, msgs, overrideIntent, overrideFilters,
    101                         overrideTechLists)) {
    102                     if (DBG) Log.d(TAG, "Dispatched to override");
    103                     return true;
    104                 }
    105                 Log.w(TAG, "Dispatch override registered, but no filters matched");
    106             } catch (CanceledException e) {
    107                 Log.w(TAG, "Dispatch overrides pending intent was canceled");
    108                 synchronized (this) {
    109                     mOverrideFilters = null;
    110                     mOverrideIntent = null;
    111                     mOverrideTechLists = null;
    112                 }
    113             }
    114         }
    115 
    116         // Try normal dispatch.
    117         try {
    118             return dispatchTagInternal(tag, msgs, null, null, null);
    119         } catch (CanceledException e) {
    120             Log.e(TAG, "CanceledException unexpected here", e);
    121             return false;
    122         }
    123     }
    124 
    125     private Intent buildTagIntent(Tag tag, NdefMessage[] msgs, String action) {
    126         Intent intent = new Intent(action);
    127         intent.putExtra(NfcAdapter.EXTRA_TAG, tag);
    128         intent.putExtra(NfcAdapter.EXTRA_ID, tag.getId());
    129         intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, msgs);
    130         return intent;
    131     }
    132 
    133     /** This method places the launched activity in a (single) NFC
    134      *  root task. We use NfcRootActivity as the root of the task,
    135      *  which launches the passed-in intent as soon as it's created.
    136      */
    137     private boolean startRootActivity(Intent intent) {
    138         Intent rootIntent = new Intent(mContext, NfcRootActivity.class);
    139         rootIntent.putExtra(NfcRootActivity.EXTRA_LAUNCH_INTENT, intent);
    140         rootIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
    141         // Ideally we'd have used startActivityForResult() to determine whether the
    142         // NfcRootActivity was able to launch the intent, but startActivityForResult()
    143         // is not available on Context. Instead, we query the PackageManager beforehand
    144         // to determine if there is an Activity to handle this intent, and base the
    145         // result of off that.
    146         List<ResolveInfo> activities = mPackageManager.queryIntentActivities(intent, 0);
    147         // Try to start the activity regardless of the result.
    148         mContext.startActivity(rootIntent);
    149         if (activities.size() > 0) {
    150             return true;
    151         } else {
    152             return false;
    153         }
    154     }
    155 
    156     // Dispatch to either an override pending intent or a standard startActivity()
    157     private boolean dispatchTagInternal(Tag tag, NdefMessage[] msgs,
    158             PendingIntent overrideIntent, IntentFilter[] overrideFilters,
    159             String[][] overrideTechLists)
    160             throws CanceledException{
    161         Intent intent;
    162 
    163         //
    164         // Try the NDEF content specific dispatch
    165         //
    166         if (msgs != null && msgs.length > 0) {
    167             NdefMessage msg = msgs[0];
    168             NdefRecord[] records = msg.getRecords();
    169             if (records.length > 0) {
    170                 // Found valid NDEF data, try to dispatch that first
    171                 NdefRecord record = records[0];
    172 
    173                 intent = buildTagIntent(tag, msgs, NfcAdapter.ACTION_NDEF_DISCOVERED);
    174                 if (setTypeOrDataFromNdef(intent, record)) {
    175                     // The record contains filterable data, try to start a matching activity
    176                     if (startDispatchActivity(intent, overrideIntent, overrideFilters,
    177                             overrideTechLists, records)) {
    178                         // If an activity is found then skip further dispatching
    179                         return true;
    180                     } else {
    181                         if (DBG) Log.d(TAG, "No activities for NDEF handling of " + intent);
    182                     }
    183                 }
    184             }
    185         }
    186 
    187         //
    188         // Try the technology specific dispatch
    189         //
    190         String[] tagTechs = tag.getTechList();
    191         Arrays.sort(tagTechs);
    192 
    193         if (overrideIntent != null) {
    194             // There are dispatch overrides in place
    195             if (overrideTechLists != null) {
    196                 for (String[] filterTechs : overrideTechLists) {
    197                     if (filterMatch(tagTechs, filterTechs)) {
    198                         // An override matched, send it to the foreground activity.
    199                         intent = buildTagIntent(tag, msgs,
    200                                 NfcAdapter.ACTION_TECH_DISCOVERED);
    201                         overrideIntent.send(mContext, Activity.RESULT_OK, intent);
    202                         return true;
    203                     }
    204                 }
    205             }
    206         } else {
    207             // Standard tech dispatch path
    208             ArrayList<ResolveInfo> matches = new ArrayList<ResolveInfo>();
    209             ArrayList<ComponentInfo> registered = mTechListFilters.getComponents();
    210 
    211             // Check each registered activity to see if it matches
    212             for (ComponentInfo info : registered) {
    213                 // Don't allow wild card matching
    214                 if (filterMatch(tagTechs, info.techs) &&
    215                         isComponentEnabled(mPackageManager, info.resolveInfo)) {
    216                     // Add the activity as a match if it's not already in the list
    217                     if (!matches.contains(info.resolveInfo)) {
    218                         matches.add(info.resolveInfo);
    219                     }
    220                 }
    221             }
    222 
    223             if (matches.size() == 1) {
    224                 // Single match, launch directly
    225                 intent = buildTagIntent(tag, msgs, NfcAdapter.ACTION_TECH_DISCOVERED);
    226                 ResolveInfo info = matches.get(0);
    227                 intent.setClassName(info.activityInfo.packageName, info.activityInfo.name);
    228                 if (startRootActivity(intent)) {
    229                     return true;
    230                 }
    231             } else if (matches.size() > 1) {
    232                 // Multiple matches, show a custom activity chooser dialog
    233                 intent = new Intent(mContext, TechListChooserActivity.class);
    234                 intent.putExtra(Intent.EXTRA_INTENT,
    235                         buildTagIntent(tag, msgs, NfcAdapter.ACTION_TECH_DISCOVERED));
    236                 intent.putParcelableArrayListExtra(TechListChooserActivity.EXTRA_RESOLVE_INFOS,
    237                         matches);
    238                 if (startRootActivity(intent)) {
    239                     return true;
    240                 }
    241             } else {
    242                 // No matches, move on
    243                 if (DBG) Log.w(TAG, "No activities for technology handling");
    244             }
    245         }
    246 
    247         //
    248         // Try the generic intent
    249         //
    250         intent = buildTagIntent(tag, msgs, NfcAdapter.ACTION_TAG_DISCOVERED);
    251         if (startDispatchActivity(intent, overrideIntent, overrideFilters, overrideTechLists,
    252                 null)) {
    253             return true;
    254         } else {
    255             Log.e(TAG, "No tag fallback activity found for " + intent);
    256             return false;
    257         }
    258     }
    259 
    260     /* Starts the package main activity if it's already installed, or takes you to its
    261      * market page if not.
    262      * returns whether an activity was started.
    263      */
    264     private boolean startActivityOrMarket(String packageName) {
    265         Intent intent = mPackageManager.getLaunchIntentForPackage(packageName);
    266         if (intent != null) {
    267             return (startRootActivity(intent));
    268         } else {
    269             // Find the package in Market:
    270             Intent market = getAppSearchIntent(packageName);
    271             return(startRootActivity(market));
    272         }
    273     }
    274 
    275     private boolean startDispatchActivity(Intent intent, PendingIntent overrideIntent,
    276             IntentFilter[] overrideFilters, String[][] overrideTechLists, NdefRecord[] records)
    277             throws CanceledException {
    278         if (overrideIntent != null) {
    279             boolean found = false;
    280             if (overrideFilters == null && overrideTechLists == null) {
    281                 // No filters means to always dispatch regardless of match
    282                 found = true;
    283             } else if (overrideFilters != null) {
    284                 for (IntentFilter filter : overrideFilters) {
    285                     if (filter.match(mContext.getContentResolver(), intent, false, TAG) >= 0) {
    286                         found = true;
    287                         break;
    288                     }
    289                 }
    290             }
    291 
    292             if (found) {
    293                 Log.i(TAG, "Dispatching to override intent " + overrideIntent);
    294                 overrideIntent.send(mContext, Activity.RESULT_OK, intent);
    295                 return true;
    296             } else {
    297                 return false;
    298             }
    299         } else {
    300             resumeAppSwitches();
    301             if (records != null) {
    302                 String firstPackage = null;
    303                 for (NdefRecord record : records) {
    304                     if (record.getTnf() == NdefRecord.TNF_EXTERNAL_TYPE) {
    305                         if (Arrays.equals(record.getType(), NdefRecord.RTD_ANDROID_APP)) {
    306                             String pkg = new String(record.getPayload(), Charsets.US_ASCII);
    307                             if (firstPackage == null) {
    308                                 firstPackage = pkg;
    309                             }
    310                             intent.setPackage(pkg);
    311                             if (startRootActivity(intent)) {
    312                                 return true;
    313                             }
    314                         }
    315                     }
    316                 }
    317                 if (firstPackage != null) {
    318                     // Found an Android package, but could not handle ndef intent.
    319                     // If the application is installed, call its main activity,
    320                     // or otherwise go to Market.
    321                     if (startActivityOrMarket(firstPackage)) {
    322                         return true;
    323                     }
    324                 }
    325             }
    326             return(startRootActivity(intent));
    327         }
    328     }
    329 
    330     /**
    331      * Tells the ActivityManager to resume allowing app switches.
    332      *
    333      * If the current app called stopAppSwitches() then our startActivity() can
    334      * be delayed for several seconds. This happens with the default home
    335      * screen.  As a system service we can override this behavior with
    336      * resumeAppSwitches().
    337     */
    338     void resumeAppSwitches() {
    339         try {
    340             mIActivityManager.resumeAppSwitches();
    341         } catch (RemoteException e) { }
    342     }
    343 
    344     /** Returns true if the tech list filter matches the techs on the tag */
    345     private boolean filterMatch(String[] tagTechs, String[] filterTechs) {
    346         if (filterTechs == null || filterTechs.length == 0) return false;
    347 
    348         for (String tech : filterTechs) {
    349             if (Arrays.binarySearch(tagTechs, tech) < 0) {
    350                 return false;
    351             }
    352         }
    353         return true;
    354     }
    355 
    356     private boolean setTypeOrDataFromNdef(Intent intent, NdefRecord record) {
    357         short tnf = record.getTnf();
    358         byte[] type = record.getType();
    359         try {
    360             switch (tnf) {
    361                 case NdefRecord.TNF_MIME_MEDIA: {
    362                     intent.setType(new String(type, Charsets.US_ASCII));
    363                     return true;
    364                 }
    365 
    366                 case NdefRecord.TNF_ABSOLUTE_URI: {
    367                     intent.setData(Uri.parse(new String(type, Charsets.UTF_8)));
    368                     return true;
    369                 }
    370 
    371                 case NdefRecord.TNF_WELL_KNOWN: {
    372                     byte[] payload = record.getPayload();
    373                     if (payload == null || payload.length == 0) return false;
    374                     if (Arrays.equals(type, NdefRecord.RTD_TEXT)) {
    375                         intent.setType("text/plain");
    376                         return true;
    377                     } else if (Arrays.equals(type, NdefRecord.RTD_SMART_POSTER)) {
    378                         // Parse the smart poster looking for the URI
    379                         try {
    380                             NdefMessage msg = new NdefMessage(record.getPayload());
    381                             for (NdefRecord subRecord : msg.getRecords()) {
    382                                 short subTnf = subRecord.getTnf();
    383                                 if (subTnf == NdefRecord.TNF_WELL_KNOWN
    384                                         && Arrays.equals(subRecord.getType(),
    385                                                 NdefRecord.RTD_URI)) {
    386                                     intent.setData(NdefRecord.parseWellKnownUriRecord(subRecord));
    387                                     return true;
    388                                 } else if (subTnf == NdefRecord.TNF_ABSOLUTE_URI) {
    389                                     intent.setData(Uri.parse(new String(subRecord.getType(),
    390                                             Charsets.UTF_8)));
    391                                     return true;
    392                                 }
    393                             }
    394                         } catch (FormatException e) {
    395                             return false;
    396                         }
    397                     } else if (Arrays.equals(type, NdefRecord.RTD_URI)) {
    398                         intent.setData(NdefRecord.parseWellKnownUriRecord(record));
    399                         return true;
    400                     }
    401                     return false;
    402                 }
    403 
    404                 case NdefRecord.TNF_EXTERNAL_TYPE: {
    405                     intent.setData(Uri.parse("vnd.android.nfc://ext/" +
    406                             new String(record.getType(), Charsets.US_ASCII)));
    407                     return true;
    408                 }
    409             }
    410             return false;
    411         } catch (Exception e) {
    412             Log.e(TAG, "failed to parse record", e);
    413             return false;
    414         }
    415     }
    416 
    417     /**
    418      * Returns an intent that can be used to find an application not currently
    419      * installed on the device.
    420      */
    421     private static Intent getAppSearchIntent(String pkg) {
    422         Intent market = new Intent(Intent.ACTION_VIEW);
    423         market.setData(Uri.parse("market://details?id=" + pkg));
    424         return market;
    425     }
    426 
    427     private static boolean isComponentEnabled(PackageManager pm, ResolveInfo info) {
    428         boolean enabled = false;
    429         ComponentName compname = new ComponentName(
    430                 info.activityInfo.packageName, info.activityInfo.name);
    431         try {
    432             // Note that getActivityInfo() will internally call
    433             // isEnabledLP() to determine whether the component
    434             // enabled. If it's not, null is returned.
    435             if (pm.getActivityInfo(compname,0) != null) {
    436                 enabled = true;
    437             }
    438         } catch (PackageManager.NameNotFoundException e) {
    439             enabled = false;
    440         }
    441         if (!enabled) {
    442             Log.d(TAG, "Component not enabled: " + compname);
    443         }
    444         return enabled;
    445     }
    446 }
    447