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