• 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 
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 
NfcDispatcher(Context context, P2pLinkManager p2pManager)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 
setForegroundDispatch(PendingIntent intent, IntentFilter[] filters, String[][] techLists)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 */
dispatchTag(Tag tag, NdefMessage[] msgs)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 
buildTagIntent(Tag tag, NdefMessage[] msgs, String action)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      */
startRootActivity(Intent intent)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()
dispatchTagInternal(Tag tag, NdefMessage[] msgs, PendingIntent overrideIntent, IntentFilter[] overrideFilters, String[][] overrideTechLists)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      */
startActivityOrMarket(String packageName)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 
startDispatchActivity(Intent intent, PendingIntent overrideIntent, IntentFilter[] overrideFilters, String[][] overrideTechLists, NdefRecord[] records)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     */
resumeAppSwitches()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 */
filterMatch(String[] tagTechs, String[] filterTechs)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 
setTypeOrDataFromNdef(Intent intent, NdefRecord record)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      */
getAppSearchIntent(String pkg)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 
isComponentEnabled(PackageManager pm, ResolveInfo info)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