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