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