Stop using URIs for ACTION_TECHNOLOGY_DISCOVERED

The pattern matching wasn't sufficient with the
move to identifying technologies with their class
names so now we use a custom dispatching mechanism.

Change-Id: I3e6379d454458bbb65730ade32cdaa1680c5e339
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index bbac974..88f1743 100755
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -14,6 +14,15 @@
     <application android:name=".NfcService"
                  android:icon="@drawable/icon"
                  android:label="@string/app_name"
-                 android:persistent="true" >
+                 android:persistent="true"
+    >
+
+        <activity android:name=".TechListChooserActivity"
+            android:theme="@*android:style/Theme.Dialog.Alert"
+            android:finishOnCloseSystemDialogs="true"
+            android:excludeFromRecents="true"
+            android:multiprocess="true"
+        />
+
     </application>
 </manifest>
diff --git a/src/com/android/nfc/NfcService.java b/src/com/android/nfc/NfcService.java
index d91c41c..70fffac 100755
--- a/src/com/android/nfc/NfcService.java
+++ b/src/com/android/nfc/NfcService.java
@@ -18,16 +18,17 @@
 
 import com.android.internal.nfc.LlcpServiceSocket;
 import com.android.internal.nfc.LlcpSocket;
+import com.android.nfc.RegisteredComponentCache.ComponentInfo;
 import com.android.nfc.ndefpush.NdefPushClient;
 import com.android.nfc.ndefpush.NdefPushServer;
 
 import android.app.Activity;
 import android.app.ActivityManagerNative;
-import android.app.IActivityManager;
 import android.app.Application;
+import android.app.IActivityManager;
 import android.app.PendingIntent;
-import android.app.StatusBarManager;
 import android.app.PendingIntent.CanceledException;
+import android.app.StatusBarManager;
 import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -35,6 +36,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.SharedPreferences;
+import android.content.pm.ResolveInfo;
 import android.net.Uri;
 import android.nfc.ErrorCodes;
 import android.nfc.FormatException;
@@ -51,6 +53,7 @@
 import android.nfc.NdefRecord;
 import android.nfc.NfcAdapter;
 import android.nfc.Tag;
+import android.nfc.TechListParcel;
 import android.nfc.TransceiveResult;
 import android.os.AsyncTask;
 import android.os.Bundle;
@@ -67,6 +70,7 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.nio.charset.Charsets;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -213,8 +217,9 @@
     static final int MSG_SE_FIELD_DEACTIVATED = 9;
 
     // Locked on mNfcAdapter
-    IntentFilter[] mDispatchOverrideFilters;
     PendingIntent mDispatchOverrideIntent;
+    IntentFilter[] mDispatchOverrideFilters;
+    String[][] mDispatchOverrideTechLists; 
 
     // TODO: none of these appear to be synchronized but are
     // read/written from different threads (notably Binder threads)...
@@ -245,6 +250,7 @@
     private IActivityManager mIActivityManager;
     NdefPushClient mNdefPushClient;
     NdefPushServer mNdefPushServer;
+    RegisteredComponentCache mTechListFilters;
 
     private static NfcService sService;
 
@@ -267,6 +273,9 @@
         mNdefPushClient = new NdefPushClient(this);
         mNdefPushServer = new NdefPushServer();
 
+        mTechListFilters = new RegisteredComponentCache(this,
+                NfcAdapter.ACTION_TECHNOLOGY_DISCOVERED, NfcAdapter.ACTION_TECHNOLOGY_DISCOVERED);
+
         mSecureElement = new NativeNfcSecureElement();
 
         mPrefs = mContext.getSharedPreferences(PREF, Context.MODE_PRIVATE);
@@ -353,7 +362,7 @@
 
         @Override
         public void enableForegroundDispatch(ComponentName activity, PendingIntent intent,
-                IntentFilter[] filters) {
+                IntentFilter[] filters, TechListParcel techListsParcel) {
             // Permission check
             mContext.enforceCallingOrSelfPermission(NFC_PERM, NFC_PERM_ERROR);
 
@@ -375,12 +384,19 @@
                 }
             }
 
+            // Validate the tech lists
+            String[][] techLists = null;
+            if (techListsParcel != null) {
+                techLists = techListsParcel.getTechLists();
+            }
+            
             synchronized (this) {
                 if (mDispatchOverrideIntent != null) {
                     Log.e(TAG, "Replacing active dispatch overrides");
                 }
                 mDispatchOverrideIntent = intent;
                 mDispatchOverrideFilters = filters;
+                mDispatchOverrideTechLists = techLists;
             }
         }
 
@@ -2566,18 +2582,6 @@
             }
         }
 
-        private Uri buildTechListUri(Tag tag) {
-            int[] techList = tag.getTechnologyList();
-            Arrays.sort(techList);
-            Uri.Builder builder = new Uri.Builder();
-            builder.scheme("vnd.android.nfc").authority("tag");
-            for (int tech : techList) {
-                builder.appendPath(Integer.toString(tech));
-            }
-            builder.appendPath("");
-            return builder.build();
-        }
-
         /** Returns false if no activities were found to dispatch to */
         private boolean dispatchTag(Tag tag, NdefMessage[] msgs) {
             if (DBG) {
@@ -2587,17 +2591,20 @@
 
             IntentFilter[] overrideFilters;
             PendingIntent overrideIntent;
+            String[][] overrideTechLists;
             boolean foregroundNdefPush = mNdefPushClient.getForegroundMessage() != null;
             synchronized (mNfcAdapter) {
                 overrideFilters = mDispatchOverrideFilters;
                 overrideIntent = mDispatchOverrideIntent;
+                overrideTechLists = mDispatchOverrideTechLists;
             }
 
             // First look for dispatch overrides
             if (overrideIntent != null) {
                 if (DBG) Log.d(TAG, "Attempting to dispatch tag with override");
                 try { 
-                    if (dispatchTagInternal(tag, msgs, overrideIntent, overrideFilters)) {
+                    if (dispatchTagInternal(tag, msgs, overrideIntent, overrideFilters,
+                            overrideTechLists)) {
                         if (DBG) Log.d(TAG, "Dispatched to override");
                         return true;
                     }
@@ -2607,6 +2614,7 @@
                     synchronized (mNfcAdapter) {
                         mDispatchOverrideFilters = null;
                         mDispatchOverrideIntent = null;
+                        mDispatchOverrideTechLists = null;
                     }
                 }
             }
@@ -2618,7 +2626,7 @@
             // remote device and the apps swapping which is in the foreground on each phone.
             if (!foregroundNdefPush) {
                 try {
-                    return dispatchTagInternal(tag, msgs, null, null);
+                    return dispatchTagInternal(tag, msgs, null, null, null);
                 } catch (CanceledException e) {
                     Log.e(TAG, "CanceledException unexpected here", e);
                     return false;
@@ -2628,11 +2636,28 @@
             return false;
         }
 
+        /** Returns true if the tech list filter matches the techs on the tag */
+        private boolean filterMatch(String[] tagTechs, String[] filterTechs) {
+            if (filterTechs == null || filterTechs.length == 0) return false;
+
+            for (String tech : filterTechs) {
+                if (Arrays.binarySearch(tagTechs, tech) < 0) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
         // Dispatch to either an override pending intent or a standard startActivity()
         private boolean dispatchTagInternal(Tag tag, NdefMessage[] msgs,
-                PendingIntent overrideIntent, IntentFilter[] overrideFilters)
+                PendingIntent overrideIntent, IntentFilter[] overrideFilters,
+                String[][] overrideTechLists)
                 throws CanceledException{
             Intent intent;
+
+            //
+            // Try the NDEF content specific dispatch
+            //
             if (msgs != null && msgs.length > 0) {
                 NdefMessage msg = msgs[0];
                 NdefRecord[] records = msg.getRecords();
@@ -2643,7 +2668,8 @@
                     intent = buildTagIntent(tag, msgs, NfcAdapter.ACTION_NDEF_DISCOVERED);
                     if (setTypeOrDataFromNdef(intent, record)) {
                         // The record contains filterable data, try to start a matching activity
-                        if (startDispatchActivity(intent, overrideIntent, overrideFilters)) {
+                        if (startDispatchActivity(intent, overrideIntent, overrideFilters,
+                                overrideTechLists)) {
                             // If an activity is found then skip further dispatching
                             return true;
                         } else {
@@ -2653,18 +2679,74 @@
                 }
             }
 
+            //
             // Try the technology specific dispatch
-            intent = buildTagIntent(tag, msgs, NfcAdapter.ACTION_TECHNOLOGY_DISCOVERED);
-            intent.setData(buildTechListUri(tag));
-            if (startDispatchActivity(intent, overrideIntent, overrideFilters)) {
-                return true;
+            //
+            String[] tagTechs = tag.getTechList();
+            Arrays.sort(tagTechs);
+
+            if (overrideIntent != null) {
+                // There are dispatch overrides in place
+                if (overrideTechLists != null) {
+                    for (String[] filterTechs : overrideTechLists) {
+                        if (filterMatch(tagTechs, filterTechs)) {
+                            // An override matched, send it to the foreground activity.
+                            intent = buildTagIntent(tag, msgs,
+                                    NfcAdapter.ACTION_TECHNOLOGY_DISCOVERED);
+                            overrideIntent.send(mContext, Activity.RESULT_OK, intent);
+                            return true;
+                        }
+                    }
+                }
             } else {
-                if (DBG) Log.w(TAG, "No activities for technology handling of " + intent);
+                // Standard tech dispatch path
+                ArrayList<ResolveInfo> matches = new ArrayList<ResolveInfo>();
+                ArrayList<ComponentInfo> registered = mTechListFilters.getComponents();
+    
+                // Check each registered activity to see if it matches
+                for (ComponentInfo info : registered) {
+                    // Don't allow wild card matching
+                    if (filterMatch(tagTechs, info.techs)) {
+                        matches.add(info.resolveInfo);
+                    }
+                }
+    
+                if (matches.size() == 1) {
+                    // Single match, launch directly
+                    intent = buildTagIntent(tag, msgs, NfcAdapter.ACTION_TECHNOLOGY_DISCOVERED);
+                    ResolveInfo info = matches.get(0);
+                    intent.setClassName(info.activityInfo.packageName, info.activityInfo.name);
+                    try {
+                        mContext.startActivity(intent);
+                        return true;
+                    } catch (ActivityNotFoundException e) {
+                        if (DBG) Log.w(TAG, "No activities for technology handling of " + intent);
+                    }
+                } else if (matches.size() > 1) {
+                    // Multiple matches, show a custom activity chooser dialog
+                    intent = new Intent(mContext, TechListChooserActivity.class);
+                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                    intent.putExtra(Intent.EXTRA_INTENT,
+                            buildTagIntent(tag, msgs, NfcAdapter.ACTION_TECHNOLOGY_DISCOVERED));
+                    intent.putParcelableArrayListExtra(TechListChooserActivity.EXTRA_RESOLVE_INFOS,
+                            matches);
+                    try {
+                        mContext.startActivity(intent);
+                        return true;
+                    } catch (ActivityNotFoundException e) {
+                        if (DBG) Log.w(TAG, "No activities for technology handling of " + intent);
+                    }
+                } else {
+                    // No matches, move on
+                    if (DBG) Log.w(TAG, "No activities for technology handling of " + intent);
+                }
             }
 
+            //
             // Try the generic intent
+            //
             intent = buildTagIntent(tag, msgs, NfcAdapter.ACTION_TAG_DISCOVERED);
-            if (startDispatchActivity(intent, overrideIntent, overrideFilters)) {
+            if (startDispatchActivity(intent, overrideIntent, overrideFilters, overrideTechLists)) {
                 return true;
             } else {
                 Log.e(TAG, "No tag fallback activity found for " + intent);
@@ -2673,13 +2755,14 @@
         }
 
         private boolean startDispatchActivity(Intent intent, PendingIntent overrideIntent,
-                IntentFilter[] overrideFilters) throws CanceledException {
+                IntentFilter[] overrideFilters, String[][] overrideTechLists)
+                throws CanceledException {
             if (overrideIntent != null) {
                 boolean found = false;
-                if (overrideFilters == null) {
+                if (overrideFilters == null && overrideTechLists == null) {
                     // No filters means to always dispatch regardless of match
                     found = true;
-                } else {
+                } else if (overrideFilters != null) {
                     for (IntentFilter filter : overrideFilters) {
                         if (filter.match(mContext.getContentResolver(), intent, false, TAG) >= 0) {
                             found = true;
diff --git a/src/com/android/nfc/RegisteredComponentCache.java b/src/com/android/nfc/RegisteredComponentCache.java
new file mode 100644
index 0000000..93a4cd1
--- /dev/null
+++ b/src/com/android/nfc/RegisteredComponentCache.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.nfc;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A cache of intent filters registered to receive the TECH_DISCOVERED dispatch.
+ */
+public class RegisteredComponentCache {
+    private static final String TAG = "RegisteredComponentCache";
+
+    final Context mContext;
+    final String mAction;
+    final String mMetaDataName;
+    final AtomicReference<BroadcastReceiver> mReceiver;
+
+    // synchronized on this
+    private ArrayList<ComponentInfo> mComponents;
+
+    public RegisteredComponentCache(Context context, String action, String metaDataName) {
+        mContext = context;
+        mAction = action;
+        mMetaDataName = metaDataName;
+
+        generateComponentsList();
+
+        final BroadcastReceiver receiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context1, Intent intent) {
+                generateComponentsList();
+            }
+        };
+        mReceiver = new AtomicReference<BroadcastReceiver>(receiver);
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+        intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        intentFilter.addDataScheme("package");
+        mContext.registerReceiver(receiver, intentFilter);
+        // Register for events related to sdcard installation.
+        IntentFilter sdFilter = new IntentFilter();
+        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
+        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
+        mContext.registerReceiver(receiver, sdFilter);
+    }
+
+    public static class ComponentInfo {
+        public final ResolveInfo resolveInfo;
+        public final String[] techs;
+
+        ComponentInfo(ResolveInfo resolveInfo, String[] techs) {
+            this.resolveInfo = resolveInfo;
+            this.techs = techs;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder out = new StringBuilder("ComponentInfo: ");
+            out.append(resolveInfo);
+            out.append(", techs: ");
+            for (String tech : techs) {
+                out.append(tech);
+                out.append(", ");
+            }
+            return out.toString();
+        }
+    }
+
+    /**
+     * @return a collection of {@link RegisteredComponentCache.ComponentInfo} objects for all
+     * registered authenticators.
+     */
+    public ArrayList<ComponentInfo> getComponents() {
+        synchronized (this) {
+            // It's safe to return a reference here since mComponents is always replaced and
+            // never updated when it changes.
+            return mComponents;
+        }
+    }
+
+    /**
+     * Stops the monitoring of package additions, removals and changes.
+     */
+    public void close() {
+        final BroadcastReceiver receiver = mReceiver.getAndSet(null);
+        if (receiver != null) {
+            mContext.unregisterReceiver(receiver);
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        if (mReceiver.get() != null) {
+            Log.e(TAG, "RegisteredServicesCache finalized without being closed");
+        }
+        close();
+        super.finalize();
+    }
+
+    void dump(ArrayList<ComponentInfo> components) {
+        for (ComponentInfo component : components) {
+            Log.i(TAG, component.toString());
+        }
+    }
+
+    void generateComponentsList() {
+        PackageManager pm = mContext.getPackageManager();
+        ArrayList<ComponentInfo> components = new ArrayList<ComponentInfo>();
+        List<ResolveInfo> resolveInfos = pm.queryIntentActivities(new Intent(mAction),
+                PackageManager.GET_META_DATA);
+        for (ResolveInfo resolveInfo : resolveInfos) {
+            try {
+                parseComponentInfo(resolveInfo, components);
+            } catch (XmlPullParserException e) {
+                Log.w(TAG, "Unable to load component info " + resolveInfo.toString(), e);
+            } catch (IOException e) {
+                Log.w(TAG, "Unable to load component info " + resolveInfo.toString(), e);
+            }
+        }
+
+        dump(components);
+
+        synchronized (this) {
+            mComponents = components;
+        }
+    }
+
+    void parseComponentInfo(ResolveInfo info, ArrayList<ComponentInfo> components)
+            throws XmlPullParserException, IOException {
+        ActivityInfo ai = info.activityInfo;
+        PackageManager pm = mContext.getPackageManager();
+
+        XmlResourceParser parser = null;
+        try {
+            parser = ai.loadXmlMetaData(pm, mMetaDataName);
+            if (parser == null) {
+                throw new XmlPullParserException("No " + mMetaDataName + " meta-data");
+            }
+
+            parseTechLists(pm.getResourcesForApplication(ai.applicationInfo), ai.packageName,
+                    parser, info, components);
+        } catch (NameNotFoundException e) {
+            throw new XmlPullParserException("Unable to load resources for " + ai.packageName);
+        } finally {
+            if (parser != null) parser.close();
+        }
+    }
+
+    void parseTechLists(Resources res, String packageName, XmlPullParser parser,
+            ResolveInfo resolveInfo, ArrayList<ComponentInfo> components)
+            throws XmlPullParserException, IOException {
+        int eventType = parser.getEventType();
+        while (eventType != XmlPullParser.START_TAG) {
+            eventType = parser.next();
+        }
+
+        ArrayList<String> items = new ArrayList();
+        String tagName;
+        eventType = parser.next();
+        do {
+            tagName = parser.getName();
+            if (eventType == XmlPullParser.START_TAG && "tech".equals(tagName)) {
+                items.add(parser.nextText());
+            } else if (eventType == XmlPullParser.END_TAG && "tech-list".equals(tagName)) {
+                int size = items.size();
+                if (size > 0) {
+                    String[] techs = new String[size];
+                    techs = items.toArray(techs);
+                    items.clear();
+                    components.add(new ComponentInfo(resolveInfo, techs));
+                }
+            }
+            eventType = parser.next();
+        } while (eventType != XmlPullParser.END_DOCUMENT);
+    }
+}
diff --git a/src/com/android/nfc/TechListChooserActivity.java b/src/com/android/nfc/TechListChooserActivity.java
new file mode 100644
index 0000000..8c2c34e
--- /dev/null
+++ b/src/com/android/nfc/TechListChooserActivity.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.nfc;
+
+import com.android.internal.app.ResolverActivity;
+
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+public class TechListChooserActivity extends ResolverActivity {
+    public static final String EXTRA_RESOLVE_INFOS = "rlist";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        Intent intent = getIntent();
+        Parcelable targetParcelable = intent.getParcelableExtra(Intent.EXTRA_INTENT);
+        if (!(targetParcelable instanceof Intent)) {
+            Log.w("TechListChooserActivity", "Target is not an intent: " + targetParcelable);
+            finish();
+            return;
+        }
+        Intent target = (Intent)targetParcelable;
+        ArrayList<ResolveInfo> rList = intent.getParcelableArrayListExtra(EXTRA_RESOLVE_INFOS);
+        CharSequence title = getResources().getText(com.android.internal.R.string.chooseActivity);
+        super.onCreate(savedInstanceState, target, title, null, rList, false);
+    }
+}