• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.Charsets;
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 
NfcDispatcher(Context context, HandoverManager handoverManager, boolean provisionOnly)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 
setForegroundDispatch(PendingIntent intent, IntentFilter[] filters, String[][] techLists)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 
disableProvisioningMode()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 
DispatchInfo(Context context, Tag tag, NdefMessage message)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 
setNdefIntent()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 
setTechIntent()155         public Intent setTechIntent() {
156             intent.setData(null);
157             intent.setType(null);
158             intent.setAction(NfcAdapter.ACTION_TECH_DISCOVERED);
159             return intent;
160         }
161 
setTagIntent()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          */
tryStartActivity()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 
tryStartActivity(Intent intentToStart)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 */
dispatchTag(Tag tag)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 
tryOverrides(DispatchInfo dispatch, Tag tag, NdefMessage message, PendingIntent overrideIntent, IntentFilter[] overrideFilters, String[][] overrideTechLists)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 
isFilterMatch(Intent intent, IntentFilter[] filters, boolean hasTechFilter)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 
isTechMatch(Tag tag, String[][] techLists)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 
tryNdef(DispatchInfo dispatch, NdefMessage message, boolean provisioningOnly)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 
extractAarPackages(NdefMessage message)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 
tryTech(DispatchInfo dispatch, Tag tag)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     */
resumeAppSwitches()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 */
filterMatch(String[] tagTechs, String[] filterTechs)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 
checkForAar(NdefRecord record)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(), Charsets.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      */
getAppSearchIntent(String pkg)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 
isComponentEnabled(PackageManager pm, ResolveInfo info)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 
dump(FileDescriptor fd, PrintWriter pw, String[] args)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