RegisteredServicesCache.java revision 31208d3ee36f583fd998c89508a3e93bb550cb29
1/*
2 * Copyright (C) 2013 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.cardemulation;
18
19import org.xmlpull.v1.XmlPullParser;
20import org.xmlpull.v1.XmlPullParserException;
21import org.xmlpull.v1.XmlSerializer;
22
23import android.app.ActivityManager;
24import android.content.BroadcastReceiver;
25import android.content.ComponentName;
26import android.content.Context;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.content.pm.PackageManager;
30import android.content.pm.ResolveInfo;
31import android.content.pm.ServiceInfo;
32import android.content.pm.PackageManager.NameNotFoundException;
33import android.nfc.cardemulation.AidGroup;
34import android.nfc.cardemulation.ApduServiceInfo;
35import android.nfc.cardemulation.CardEmulation;
36import android.nfc.cardemulation.HostApduService;
37import android.nfc.cardemulation.OffHostApduService;
38import android.os.UserHandle;
39import android.util.AtomicFile;
40import android.util.Log;
41import android.util.SparseArray;
42import android.util.Xml;
43
44import com.android.internal.util.FastXmlSerializer;
45import com.google.android.collect.Maps;
46
47import java.io.File;
48import java.io.FileDescriptor;
49import java.io.FileInputStream;
50import java.io.FileOutputStream;
51import java.io.IOException;
52import java.io.PrintWriter;
53import java.util.ArrayList;
54import java.util.Collections;
55import java.util.HashMap;
56import java.util.Iterator;
57import java.util.List;
58import java.util.Map;
59import java.util.concurrent.atomic.AtomicReference;
60
61/**
62 * This class is inspired by android.content.pm.RegisteredServicesCache
63 * That class was not re-used because it doesn't support dynamically
64 * registering additional properties, but generates everything from
65 * the manifest. Since we have some properties that are not in the manifest,
66 * it's less suited.
67 */
68public class RegisteredServicesCache {
69    static final String XML_INDENT_OUTPUT_FEATURE = "http://xmlpull.org/v1/doc/features.html#indent-output";
70    static final String TAG = "RegisteredServicesCache";
71    static final boolean DEBUG = true;
72
73    final Context mContext;
74    final AtomicReference<BroadcastReceiver> mReceiver;
75
76    final Object mLock = new Object();
77    // All variables below synchronized on mLock
78
79    // mUserServices holds the card emulation services that are running for each user
80    final SparseArray<UserServices> mUserServices = new SparseArray<UserServices>();
81    final Callback mCallback;
82    final AtomicFile mDynamicAidsFile;
83
84    public interface Callback {
85        void onServicesUpdated(int userId, final List<ApduServiceInfo> services);
86    };
87
88    static class DynamicAids {
89        public final int uid;
90        public final HashMap<String, AidGroup> aidGroups = Maps.newHashMap();
91
92        DynamicAids(int uid) {
93            this.uid = uid;
94        }
95    };
96
97    private static class UserServices {
98        /**
99         * All services that have registered
100         */
101        final HashMap<ComponentName, ApduServiceInfo> services =
102                Maps.newHashMap(); // Re-built at run-time
103        final HashMap<ComponentName, DynamicAids> dynamicAids =
104                Maps.newHashMap(); // In memory cache of dynamic AID store
105    };
106
107    private UserServices findOrCreateUserLocked(int userId) {
108        UserServices services = mUserServices.get(userId);
109        if (services == null) {
110            services = new UserServices();
111            mUserServices.put(userId, services);
112        }
113        return services;
114    }
115
116    public RegisteredServicesCache(Context context, Callback callback) {
117        mContext = context;
118        mCallback = callback;
119
120        final BroadcastReceiver receiver = new BroadcastReceiver() {
121            @Override
122            public void onReceive(Context context, Intent intent) {
123                final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
124                String action = intent.getAction();
125                if (DEBUG) Log.d(TAG, "Intent action: " + action);
126                if (uid != -1) {
127                    boolean replaced = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) &&
128                            (Intent.ACTION_PACKAGE_ADDED.equals(action) ||
129                             Intent.ACTION_PACKAGE_REMOVED.equals(action));
130                    if (!replaced) {
131                        int currentUser = ActivityManager.getCurrentUser();
132                        if (currentUser == UserHandle.getUserId(uid)) {
133                            invalidateCache(UserHandle.getUserId(uid));
134                        } else {
135                            // Cache will automatically be updated on user switch
136                        }
137                    } else {
138                        if (DEBUG) Log.d(TAG, "Ignoring package intent due to package being replaced.");
139                    }
140                }
141            }
142        };
143        mReceiver = new AtomicReference<BroadcastReceiver>(receiver);
144
145        IntentFilter intentFilter = new IntentFilter();
146        intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
147        intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
148        intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
149        intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
150        intentFilter.addAction(Intent.ACTION_PACKAGE_FIRST_LAUNCH);
151        intentFilter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
152        intentFilter.addDataScheme("package");
153        mContext.registerReceiverAsUser(mReceiver.get(), UserHandle.ALL, intentFilter, null, null);
154
155        // Register for events related to sdcard operations
156        IntentFilter sdFilter = new IntentFilter();
157        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
158        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
159        mContext.registerReceiverAsUser(mReceiver.get(), UserHandle.ALL, sdFilter, null, null);
160
161        File dataDir = mContext.getFilesDir();
162        mDynamicAidsFile = new AtomicFile(new File(dataDir, "dynamic_aids.xml"));
163    }
164
165    void initialize() {
166        synchronized (mLock) {
167            readDynamicAidsLocked();
168        }
169        invalidateCache(ActivityManager.getCurrentUser());
170    }
171
172    void dump(ArrayList<ApduServiceInfo> services) {
173        for (ApduServiceInfo service : services) {
174            if (DEBUG) Log.d(TAG, service.toString());
175        }
176    }
177
178    boolean containsServiceLocked(ArrayList<ApduServiceInfo> services, ComponentName serviceName) {
179        for (ApduServiceInfo service : services) {
180            if (service.getComponent().equals(serviceName)) return true;
181        }
182        return false;
183    }
184
185    public boolean hasService(int userId, ComponentName service) {
186        return getService(userId, service) != null;
187    }
188
189    public ApduServiceInfo getService(int userId, ComponentName service) {
190        synchronized (mLock) {
191            UserServices userServices = findOrCreateUserLocked(userId);
192            return userServices.services.get(service);
193        }
194    }
195
196    public List<ApduServiceInfo> getServices(int userId) {
197        final ArrayList<ApduServiceInfo> services = new ArrayList<ApduServiceInfo>();
198        synchronized (mLock) {
199            UserServices userServices = findOrCreateUserLocked(userId);
200            services.addAll(userServices.services.values());
201        }
202        return services;
203    }
204
205    public List<ApduServiceInfo> getServicesForCategory(int userId, String category) {
206        final ArrayList<ApduServiceInfo> services = new ArrayList<ApduServiceInfo>();
207        synchronized (mLock) {
208            UserServices userServices = findOrCreateUserLocked(userId);
209            for (ApduServiceInfo service : userServices.services.values()) {
210                if (service.hasCategory(category)) services.add(service);
211            }
212        }
213        return services;
214    }
215
216    ArrayList<ApduServiceInfo> getInstalledServices(int userId) {
217        PackageManager pm;
218        try {
219            pm = mContext.createPackageContextAsUser("android", 0,
220                    new UserHandle(userId)).getPackageManager();
221        } catch (NameNotFoundException e) {
222            Log.e(TAG, "Could not create user package context");
223            return null;
224        }
225
226        ArrayList<ApduServiceInfo> validServices = new ArrayList<ApduServiceInfo>();
227
228        List<ResolveInfo> resolvedServices = pm.queryIntentServicesAsUser(
229                new Intent(HostApduService.SERVICE_INTERFACE),
230                PackageManager.GET_META_DATA, userId);
231
232        List<ResolveInfo> resolvedOffHostServices = pm.queryIntentServicesAsUser(
233                new Intent(OffHostApduService.SERVICE_INTERFACE),
234                PackageManager.GET_META_DATA, userId);
235        resolvedServices.addAll(resolvedOffHostServices);
236
237        for (ResolveInfo resolvedService : resolvedServices) {
238            try {
239                boolean onHost = !resolvedOffHostServices.contains(resolvedService);
240                ServiceInfo si = resolvedService.serviceInfo;
241                ComponentName componentName = new ComponentName(si.packageName, si.name);
242                // Check if the package holds the NFC permission
243                if (pm.checkPermission(android.Manifest.permission.NFC, si.packageName) !=
244                        PackageManager.PERMISSION_GRANTED) {
245                    Log.e(TAG, "Skipping APDU service " + componentName +
246                            ": it does not require the permission " +
247                            android.Manifest.permission.NFC);
248                    continue;
249                }
250                if (!android.Manifest.permission.BIND_NFC_SERVICE.equals(
251                        si.permission)) {
252                    Log.e(TAG, "Skipping APDU service " + componentName +
253                            ": it does not require the permission " +
254                            android.Manifest.permission.BIND_NFC_SERVICE);
255                    continue;
256                }
257                ApduServiceInfo service = new ApduServiceInfo(pm, resolvedService, onHost);
258                if (service != null) {
259                    validServices.add(service);
260                }
261            } catch (XmlPullParserException e) {
262                Log.w(TAG, "Unable to load component info " + resolvedService.toString(), e);
263            } catch (IOException e) {
264                Log.w(TAG, "Unable to load component info " + resolvedService.toString(), e);
265            }
266        }
267
268        return validServices;
269    }
270
271    public void invalidateCache(int userId) {
272        final ArrayList<ApduServiceInfo> validServices = getInstalledServices(userId);
273        if (validServices == null) {
274            return;
275        }
276        synchronized (mLock) {
277            UserServices userServices = findOrCreateUserLocked(userId);
278
279            // Find removed services
280            Iterator<Map.Entry<ComponentName, ApduServiceInfo>> it =
281                    userServices.services.entrySet().iterator();
282            while (it.hasNext()) {
283                Map.Entry<ComponentName, ApduServiceInfo> entry =
284                        (Map.Entry<ComponentName, ApduServiceInfo>) it.next();
285                if (!containsServiceLocked(validServices, entry.getKey())) {
286                    Log.d(TAG, "Service removed: " + entry.getKey());
287                    it.remove();
288                }
289            }
290            for (ApduServiceInfo service : validServices) {
291                if (DEBUG) Log.d(TAG, "Adding service: " + service.getComponent() +
292                        " AIDs: " + service.getAids());
293                userServices.services.put(service.getComponent(), service);
294            }
295
296            // Apply dynamic AID mappings
297            ArrayList<ComponentName> toBeRemoved = new ArrayList<ComponentName>();
298            for (Map.Entry<ComponentName, DynamicAids> entry :
299                    userServices.dynamicAids.entrySet()) {
300                // Verify component / uid match
301                ComponentName component = entry.getKey();
302                DynamicAids dynamicAids = entry.getValue();
303                ApduServiceInfo serviceInfo = userServices.services.get(component);
304                if (serviceInfo == null || (serviceInfo.getUid() != dynamicAids.uid)) {
305                    toBeRemoved.add(component);
306                    continue;
307                } else {
308                    for (AidGroup group : dynamicAids.aidGroups.values()) {
309                        serviceInfo.setOrReplaceDynamicAidGroup(group);
310                    }
311                }
312            }
313
314            if (toBeRemoved.size() > 0) {
315                for (ComponentName component : toBeRemoved) {
316                    Log.d(TAG, "Removing dynamic AIDs registered by " + component);
317                    userServices.dynamicAids.remove(component);
318                }
319                // Persist to filesystem
320                writeDynamicAidsLocked();
321            }
322        }
323
324        mCallback.onServicesUpdated(userId, Collections.unmodifiableList(validServices));
325        dump(validServices);
326    }
327
328    private void readDynamicAidsLocked() {
329        FileInputStream fis = null;
330        try {
331            if (!mDynamicAidsFile.getBaseFile().exists()) {
332                Log.d(TAG, "Dynamic AIDs file does not exist.");
333                return;
334            }
335            fis = mDynamicAidsFile.openRead();
336            XmlPullParser parser = Xml.newPullParser();
337            parser.setInput(fis, null);
338            int eventType = parser.getEventType();
339            while (eventType != XmlPullParser.START_TAG &&
340                    eventType != XmlPullParser.END_DOCUMENT) {
341                eventType = parser.next();
342            }
343            String tagName = parser.getName();
344            if ("services".equals(tagName)) {
345                boolean inService = false;
346                ComponentName currentComponent = null;
347                int currentUid = -1;
348                ArrayList<AidGroup> currentGroups = new ArrayList<AidGroup>();
349                while (eventType != XmlPullParser.END_DOCUMENT) {
350                    tagName = parser.getName();
351                    if (eventType == XmlPullParser.START_TAG) {
352                        if ("service".equals(tagName) && parser.getDepth() == 2) {
353                            String compString = parser.getAttributeValue(null, "component");
354                            String uidString = parser.getAttributeValue(null, "uid");
355                            if (compString == null || uidString == null) {
356                                Log.e(TAG, "Invalid service attributes");
357                            } else {
358                                try {
359                                    currentUid = Integer.parseInt(uidString);
360                                    currentComponent = ComponentName.unflattenFromString(compString);
361                                    inService = true;
362                                } catch (NumberFormatException e) {
363                                    Log.e(TAG, "Could not parse service uid");
364                                }
365                            }
366                        }
367                        if ("aid-group".equals(tagName) && parser.getDepth() == 3 && inService) {
368                            AidGroup group = AidGroup.createFromXml(parser);
369                            if (group != null) {
370                                currentGroups.add(group);
371                            } else {
372                                Log.e(TAG, "Could not parse AID group.");
373                            }
374                        }
375                    } else if (eventType == XmlPullParser.END_TAG) {
376                        if ("service".equals(tagName)) {
377                            // See if we have a valid service
378                            if (currentComponent != null && currentUid >= 0 &&
379                                    currentGroups.size() > 0) {
380                                final int userId = UserHandle.getUserId(currentUid);
381                                DynamicAids dynAids = new DynamicAids(currentUid);
382                                for (AidGroup group : currentGroups) {
383                                    dynAids.aidGroups.put(group.getCategory(), group);
384                                }
385                                UserServices services = findOrCreateUserLocked(userId);
386                                services.dynamicAids.put(currentComponent, dynAids);
387                            }
388                            currentUid = -1;
389                            currentComponent = null;
390                            currentGroups.clear();
391                            inService = false;
392                        }
393                    }
394                    eventType = parser.next();
395                };
396            }
397        } catch (Exception e) {
398            Log.e(TAG, "Could not parse dynamic AIDs file, trashing.");
399            mDynamicAidsFile.delete();
400        } finally {
401            if (fis != null) {
402                try {
403                    fis.close();
404                } catch (IOException e) {
405                }
406            }
407        }
408    }
409
410    private boolean writeDynamicAidsLocked() {
411        FileOutputStream fos = null;
412        try {
413            fos = mDynamicAidsFile.startWrite();
414            XmlSerializer out = new FastXmlSerializer();
415            out.setOutput(fos, "utf-8");
416            out.startDocument(null, true);
417            out.setFeature(XML_INDENT_OUTPUT_FEATURE, true);
418            out.startTag(null, "services");
419            for (int i = 0; i < mUserServices.size(); i++) {
420                final UserServices user = mUserServices.valueAt(i);
421                for (Map.Entry<ComponentName, DynamicAids> service : user.dynamicAids.entrySet()) {
422                    out.startTag(null, "service");
423                    out.attribute(null, "component", service.getKey().flattenToString());
424                    out.attribute(null, "uid", Integer.toString(service.getValue().uid));
425                    for (AidGroup group : service.getValue().aidGroups.values()) {
426                        group.writeAsXml(out);
427                    }
428                    out.endTag(null, "service");
429                }
430            }
431            out.endTag(null, "services");
432            out.endDocument();
433            mDynamicAidsFile.finishWrite(fos);
434            return true;
435        } catch (Exception e) {
436            Log.e(TAG, "Error writing dynamic AIDs", e);
437            if (fos != null) {
438                mDynamicAidsFile.failWrite(fos);
439            }
440            return false;
441        }
442    }
443
444    public boolean registerAidGroupForService(int userId, int uid,
445            ComponentName componentName, AidGroup aidGroup) {
446        ArrayList<ApduServiceInfo> newServices = null;
447        boolean success;
448        synchronized (mLock) {
449            UserServices services = findOrCreateUserLocked(userId);
450            // Check if we can find this service
451            ApduServiceInfo serviceInfo = getService(userId, componentName);
452            if (serviceInfo == null) {
453                Log.e(TAG, "Service " + componentName + " does not exist.");
454                return false;
455            }
456            if (serviceInfo.getUid() != uid) {
457                // This is probably a good indication something is wrong here.
458                // Either newer service installed with different uid (but then
459                // we should have known about it), or somebody calling us from
460                // a different uid.
461                Log.e(TAG, "UID mismatch.");
462                return false;
463            }
464            // Do another AID validation, since a caller could have thrown in a modified
465            // AidGroup object with invalid AIDs over Binder.
466            List<String> aids = aidGroup.getAids();
467            for (String aid : aids) {
468                if (!CardEmulation.isValidAid(aid)) {
469                    Log.e(TAG, "AID " + aid + " is not a valid AID");
470                    return false;
471                }
472            }
473            serviceInfo.setOrReplaceDynamicAidGroup(aidGroup);
474            DynamicAids dynAids = services.dynamicAids.get(componentName);
475            if (dynAids == null) {
476                dynAids = new DynamicAids(uid);
477                services.dynamicAids.put(componentName, dynAids);
478            }
479            dynAids.aidGroups.put(aidGroup.getCategory(), aidGroup);
480            success = writeDynamicAidsLocked();
481            if (success) {
482                newServices = new ArrayList<ApduServiceInfo>(services.services.values());
483            } else {
484                Log.e(TAG, "Failed to persist AID group.");
485                // Undo registration
486                dynAids.aidGroups.remove(aidGroup.getCategory());
487            }
488        }
489        if (success) {
490            // Make callback without the lock held
491            mCallback.onServicesUpdated(userId, newServices);
492        }
493        return success;
494    }
495
496    public AidGroup getAidGroupForService(int userId, int uid, ComponentName componentName,
497            String category) {
498        ApduServiceInfo serviceInfo = getService(userId, componentName);
499        if (serviceInfo != null) {
500            if (serviceInfo.getUid() != uid) {
501                Log.e(TAG, "UID mismatch");
502                return null;
503            }
504            return serviceInfo.getDynamicAidGroupForCategory(category);
505        } else {
506            Log.e(TAG, "Could not find service " + componentName);
507            return null;
508        }
509    }
510
511    public boolean removeAidGroupForService(int userId, int uid, ComponentName componentName,
512            String category) {
513        boolean success = false;
514        ArrayList<ApduServiceInfo> newServices = null;
515        synchronized (mLock) {
516            UserServices services = findOrCreateUserLocked(userId);
517            ApduServiceInfo serviceInfo = getService(userId, componentName);
518            if (serviceInfo != null) {
519                if (serviceInfo.getUid() != uid) {
520                    // Calling from different uid
521                    Log.e(TAG, "UID mismatch");
522                    return false;
523                }
524                if (!serviceInfo.removeDynamicAidGroupForCategory(category)) {
525                    Log.e(TAG," Could not find dynamic AIDs for category " + category);
526                    return false;
527                }
528                // Remove from local cache
529                DynamicAids dynAids = services.dynamicAids.get(componentName);
530                if (dynAids != null) {
531                    AidGroup deletedGroup = dynAids.aidGroups.remove(category);
532                    success = writeDynamicAidsLocked();
533                    if (success) {
534                        newServices = new ArrayList<ApduServiceInfo>(services.services.values());
535                    } else {
536                        Log.e(TAG, "Could not persist deleted AID group.");
537                        dynAids.aidGroups.put(category, deletedGroup);
538                        return false;
539                    }
540                } else {
541                    Log.e(TAG, "Could not find aid group in local cache.");
542                }
543            } else {
544                Log.e(TAG, "Service " + componentName + " does not exist.");
545            }
546        }
547        if (success) {
548            mCallback.onServicesUpdated(userId, newServices);
549        }
550        return success;
551    }
552
553    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
554        pw.println("Registered HCE services for current user: ");
555        UserServices userServices = findOrCreateUserLocked(ActivityManager.getCurrentUser());
556        for (ApduServiceInfo service : userServices.services.values()) {
557            service.dump(fd, pw, args);
558            pw.println("");
559        }
560        pw.println("");
561    }
562
563}
564