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