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.Context;
28import android.content.Intent;
29import android.content.IntentFilter;
30import android.content.pm.PackageManager;
31import android.content.pm.ResolveInfo;
32import android.net.Uri;
33import android.nfc.FormatException;
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.nio.charset.Charsets;
42import java.util.ArrayList;
43import java.util.Arrays;
44import java.util.List;
45
46/**
47 * Dispatch of NFC events to start activities
48 */
49public 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
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
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 */
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
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     */
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()
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     */
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
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    */
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 */
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
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     */
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
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