ApduServiceInfo.java revision 1bfc3d624925c2b6e0928e26cf0660aee0a05ddf
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 android.nfc.cardemulation;
18
19import android.content.ComponentName;
20import android.content.pm.PackageManager;
21import android.content.pm.ResolveInfo;
22import android.content.pm.ServiceInfo;
23import android.content.pm.PackageManager.NameNotFoundException;
24import android.content.res.Resources;
25import android.content.res.Resources.NotFoundException;
26import android.content.res.TypedArray;
27import android.content.res.XmlResourceParser;
28import android.graphics.drawable.Drawable;
29import android.os.Parcel;
30import android.os.Parcelable;
31import android.util.AttributeSet;
32import android.util.Log;
33import android.util.Xml;
34
35import org.xmlpull.v1.XmlPullParser;
36import org.xmlpull.v1.XmlPullParserException;
37
38import java.io.FileDescriptor;
39import java.io.IOException;
40import java.io.PrintWriter;
41import java.util.ArrayList;
42import java.util.HashMap;
43import java.util.Map;
44
45/**
46 * @hide
47 */
48public final class ApduServiceInfo implements Parcelable {
49    static final String TAG = "ApduServiceInfo";
50
51    /**
52     * The service that implements this
53     */
54    final ResolveInfo mService;
55
56    /**
57     * Description of the service
58     */
59    final String mDescription;
60
61    /**
62     * Whether this service represents AIDs running on the host CPU
63     */
64    final boolean mOnHost;
65
66    /**
67     * Mapping from category to static AID group
68     */
69    final HashMap<String, AidGroup> mStaticAidGroups;
70
71    /**
72     * Mapping from category to dynamic AID group
73     */
74    final HashMap<String, AidGroup> mDynamicAidGroups;
75
76    /**
77     * Whether this service should only be started when the device is unlocked.
78     */
79    final boolean mRequiresDeviceUnlock;
80
81    /**
82     * The id of the service banner specified in XML.
83     */
84    final int mBannerResourceId;
85
86    /**
87     * The uid of the package the service belongs to
88     */
89    final int mUid;
90    /**
91     * @hide
92     */
93    public ApduServiceInfo(ResolveInfo info, boolean onHost, String description,
94            ArrayList<AidGroup> staticAidGroups, ArrayList<AidGroup> dynamicAidGroups,
95            boolean requiresUnlock, int bannerResource, int uid) {
96        this.mService = info;
97        this.mDescription = description;
98        this.mStaticAidGroups = new HashMap<String, AidGroup>();
99        this.mDynamicAidGroups = new HashMap<String, AidGroup>();
100        this.mOnHost = onHost;
101        this.mRequiresDeviceUnlock = requiresUnlock;
102        for (AidGroup aidGroup : staticAidGroups) {
103            this.mStaticAidGroups.put(aidGroup.category, aidGroup);
104        }
105        for (AidGroup aidGroup : dynamicAidGroups) {
106            this.mDynamicAidGroups.put(aidGroup.category, aidGroup);
107        }
108        this.mBannerResourceId = bannerResource;
109        this.mUid = uid;
110    }
111
112    public ApduServiceInfo(PackageManager pm, ResolveInfo info, boolean onHost) throws
113            XmlPullParserException, IOException {
114        ServiceInfo si = info.serviceInfo;
115        XmlResourceParser parser = null;
116        try {
117            if (onHost) {
118                parser = si.loadXmlMetaData(pm, HostApduService.SERVICE_META_DATA);
119                if (parser == null) {
120                    throw new XmlPullParserException("No " + HostApduService.SERVICE_META_DATA +
121                            " meta-data");
122                }
123            } else {
124                parser = si.loadXmlMetaData(pm, OffHostApduService.SERVICE_META_DATA);
125                if (parser == null) {
126                    throw new XmlPullParserException("No " + OffHostApduService.SERVICE_META_DATA +
127                            " meta-data");
128                }
129            }
130
131            int eventType = parser.getEventType();
132            while (eventType != XmlPullParser.START_TAG && eventType != XmlPullParser.END_DOCUMENT) {
133                eventType = parser.next();
134            }
135
136            String tagName = parser.getName();
137            if (onHost && !"host-apdu-service".equals(tagName)) {
138                throw new XmlPullParserException(
139                        "Meta-data does not start with <host-apdu-service> tag");
140            } else if (!onHost && !"offhost-apdu-service".equals(tagName)) {
141                throw new XmlPullParserException(
142                        "Meta-data does not start with <offhost-apdu-service> tag");
143            }
144
145            Resources res = pm.getResourcesForApplication(si.applicationInfo);
146            AttributeSet attrs = Xml.asAttributeSet(parser);
147            if (onHost) {
148                TypedArray sa = res.obtainAttributes(attrs,
149                        com.android.internal.R.styleable.HostApduService);
150                mService = info;
151                mDescription = sa.getString(
152                        com.android.internal.R.styleable.HostApduService_description);
153                mRequiresDeviceUnlock = sa.getBoolean(
154                        com.android.internal.R.styleable.HostApduService_requireDeviceUnlock,
155                        false);
156                mBannerResourceId = sa.getResourceId(
157                        com.android.internal.R.styleable.HostApduService_apduServiceBanner, -1);
158                sa.recycle();
159            } else {
160                TypedArray sa = res.obtainAttributes(attrs,
161                        com.android.internal.R.styleable.OffHostApduService);
162                mService = info;
163                mDescription = sa.getString(
164                        com.android.internal.R.styleable.OffHostApduService_description);
165                mRequiresDeviceUnlock = false;
166                mBannerResourceId = sa.getResourceId(
167                        com.android.internal.R.styleable.OffHostApduService_apduServiceBanner, -1);
168                sa.recycle();
169            }
170
171            mStaticAidGroups = new HashMap<String, AidGroup>();
172            mDynamicAidGroups = new HashMap<String, AidGroup>();
173            mOnHost = onHost;
174
175            final int depth = parser.getDepth();
176            AidGroup currentGroup = null;
177
178            // Parsed values for the current AID group
179            while (((eventType = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
180                    && eventType != XmlPullParser.END_DOCUMENT) {
181                tagName = parser.getName();
182                if (eventType == XmlPullParser.START_TAG && "aid-group".equals(tagName) &&
183                        currentGroup == null) {
184                    final TypedArray groupAttrs = res.obtainAttributes(attrs,
185                            com.android.internal.R.styleable.AidGroup);
186                    // Get category of AID group
187                    String groupCategory = groupAttrs.getString(
188                            com.android.internal.R.styleable.AidGroup_category);
189                    String groupDescription = groupAttrs.getString(
190                            com.android.internal.R.styleable.AidGroup_description);
191                    if (!CardEmulation.CATEGORY_PAYMENT.equals(groupCategory)) {
192                        groupCategory = CardEmulation.CATEGORY_OTHER;
193                    }
194                    currentGroup = mStaticAidGroups.get(groupCategory);
195                    if (currentGroup != null) {
196                        if (!CardEmulation.CATEGORY_OTHER.equals(groupCategory)) {
197                            Log.e(TAG, "Not allowing multiple aid-groups in the " +
198                                    groupCategory + " category");
199                            currentGroup = null;
200                        }
201                    } else {
202                        currentGroup = new AidGroup(groupCategory, groupDescription);
203                    }
204                    groupAttrs.recycle();
205                } else if (eventType == XmlPullParser.END_TAG && "aid-group".equals(tagName) &&
206                        currentGroup != null) {
207                    if (currentGroup.aids.size() > 0) {
208                        if (!mStaticAidGroups.containsKey(currentGroup.category)) {
209                            mStaticAidGroups.put(currentGroup.category, currentGroup);
210                        }
211                    } else {
212                        Log.e(TAG, "Not adding <aid-group> with empty or invalid AIDs");
213                    }
214                    currentGroup = null;
215                } else if (eventType == XmlPullParser.START_TAG && "aid-filter".equals(tagName) &&
216                        currentGroup != null) {
217                    final TypedArray a = res.obtainAttributes(attrs,
218                            com.android.internal.R.styleable.AidFilter);
219                    String aid = a.getString(com.android.internal.R.styleable.AidFilter_name).
220                            toUpperCase();
221                    if (isValidAid(aid) && !currentGroup.aids.contains(aid)) {
222                        currentGroup.aids.add(aid);
223                    } else {
224                        Log.e(TAG, "Ignoring invalid or duplicate aid: " + aid);
225                    }
226                    a.recycle();
227                }
228            }
229        } catch (NameNotFoundException e) {
230            throw new XmlPullParserException("Unable to create context for: " + si.packageName);
231        } finally {
232            if (parser != null) parser.close();
233        }
234        // Set uid
235        mUid = si.applicationInfo.uid;
236    }
237
238    public ComponentName getComponent() {
239        return new ComponentName(mService.serviceInfo.packageName,
240                mService.serviceInfo.name);
241    }
242
243    /**
244     * Returns a consolidated list of AIDs from the AID groups
245     * registered by this service. Note that if a service has both
246     * a static (manifest-based) AID group for a category and a dynamic
247     * AID group, only the dynamically registered AIDs will be returned
248     * for that category.
249     * @return List of AIDs registered by the service
250     */
251    public ArrayList<String> getAids() {
252        final ArrayList<String> aids = new ArrayList<String>();
253        for (AidGroup group : getAidGroups()) {
254            aids.addAll(group.aids);
255        }
256        return aids;
257    }
258
259    /**
260     * Returns the registered AID group for this category.
261     */
262    public AidGroup getDynamicAidGroupForCategory(String category) {
263        return mDynamicAidGroups.get(category);
264    }
265
266    public boolean removeDynamicAidGroupForCategory(String category) {
267        return (mDynamicAidGroups.remove(category) != null);
268    }
269
270    /**
271     * Returns a consolidated list of AID groups
272     * registered by this service. Note that if a service has both
273     * a static (manifest-based) AID group for a category and a dynamic
274     * AID group, only the dynamically registered AID group will be returned
275     * for that category.
276     * @return List of AIDs registered by the service
277     */
278    public ArrayList<AidGroup> getAidGroups() {
279        final ArrayList<AidGroup> groups = new ArrayList<AidGroup>();
280        for (Map.Entry<String, AidGroup> entry : mDynamicAidGroups.entrySet()) {
281            groups.add(entry.getValue());
282        }
283        for (Map.Entry<String, AidGroup> entry : mStaticAidGroups.entrySet()) {
284            if (!mDynamicAidGroups.containsKey(entry.getKey())) {
285                // Consolidate AID groups - don't return static ones
286                // if a dynamic group exists for the category.
287                groups.add(entry.getValue());
288            }
289        }
290        return groups;
291    }
292
293    /**
294     * Returns the category to which this service has attributed the AID that is passed in,
295     * or null if we don't know this AID.
296     */
297    public String getCategoryForAid(String aid) {
298        ArrayList<AidGroup> groups = getAidGroups();
299        for (AidGroup group : groups) {
300            if (group.aids.contains(aid)) {
301                return group.category;
302            }
303        }
304        return null;
305    }
306
307    public boolean hasCategory(String category) {
308        return (mStaticAidGroups.containsKey(category) || mDynamicAidGroups.containsKey(category));
309    }
310
311    public boolean isOnHost() {
312        return mOnHost;
313    }
314
315    public boolean requiresUnlock() {
316        return mRequiresDeviceUnlock;
317    }
318
319    public String getDescription() {
320        return mDescription;
321    }
322
323    public int getUid() {
324        return mUid;
325    }
326
327    public void setOrReplaceDynamicAidGroup(AidGroup aidGroup) {
328        mDynamicAidGroups.put(aidGroup.getCategory(), aidGroup);
329    }
330
331    public CharSequence loadLabel(PackageManager pm) {
332        return mService.loadLabel(pm);
333    }
334
335    public Drawable loadIcon(PackageManager pm) {
336        return mService.loadIcon(pm);
337    }
338
339    public Drawable loadBanner(PackageManager pm) {
340        Resources res;
341        try {
342            res = pm.getResourcesForApplication(mService.serviceInfo.packageName);
343            Drawable banner = res.getDrawable(mBannerResourceId);
344            return banner;
345        } catch (NotFoundException e) {
346            Log.e(TAG, "Could not load banner.");
347            return null;
348        } catch (NameNotFoundException e) {
349            Log.e(TAG, "Could not load banner.");
350            return null;
351        }
352    }
353
354    /**
355     * A valid AID according to ISO/IEC 7816-4:
356     * <ul>
357     * <li>Has >= 5 bytes and <=16 bytes (>=10 hex chars and <= 32 hex chars)
358     * <li>Consist of only hex characters
359     * <li>Additionally, we allow an asterisk at the end, to indicate
360     *     a prefix
361     * </ul>
362     */
363    static boolean isValidAid(String aid) {
364        if (aid == null)
365            return false;
366
367        // If a prefix AID, the total length must be odd (even # of AID chars + '*')
368        if (aid.endsWith("*") && ((aid.length() % 2) == 0)) {
369            Log.e(TAG, "AID " + aid + " is not a valid AID.");
370            return false;
371        }
372
373        // If not a prefix AID, the total length must be even (even # of AID chars)
374        if (!aid.endsWith("*") && ((aid.length() % 2) != 0)) {
375            Log.e(TAG, "AID " + aid + " is not a valid AID.");
376            return false;
377        }
378
379        // Verify hex characters
380        if (!aid.matches("[0-9A-Fa-f]{10,32}\\*?")) {
381            Log.e(TAG, "AID " + aid + " is not a valid AID.");
382            return false;
383        }
384
385        return true;
386    }
387
388    @Override
389    public String toString() {
390        StringBuilder out = new StringBuilder("ApduService: ");
391        out.append(getComponent());
392        out.append(", description: " + mDescription);
393        out.append(", Static AID Groups: ");
394        for (AidGroup aidGroup : mStaticAidGroups.values()) {
395            out.append(aidGroup.toString());
396        }
397        out.append(", Dynamic AID Groups: ");
398        for (AidGroup aidGroup : mDynamicAidGroups.values()) {
399            out.append(aidGroup.toString());
400        }
401        return out.toString();
402    }
403
404    @Override
405    public boolean equals(Object o) {
406        if (this == o) return true;
407        if (!(o instanceof ApduServiceInfo)) return false;
408        ApduServiceInfo thatService = (ApduServiceInfo) o;
409
410        return thatService.getComponent().equals(this.getComponent());
411    }
412
413    @Override
414    public int hashCode() {
415        return getComponent().hashCode();
416    }
417
418
419    @Override
420    public int describeContents() {
421        return 0;
422    }
423
424    @Override
425    public void writeToParcel(Parcel dest, int flags) {
426        mService.writeToParcel(dest, flags);
427        dest.writeString(mDescription);
428        dest.writeInt(mOnHost ? 1 : 0);
429        dest.writeInt(mStaticAidGroups.size());
430        if (mStaticAidGroups.size() > 0) {
431            dest.writeTypedList(new ArrayList<AidGroup>(mStaticAidGroups.values()));
432        }
433        dest.writeInt(mDynamicAidGroups.size());
434        if (mDynamicAidGroups.size() > 0) {
435            dest.writeTypedList(new ArrayList<AidGroup>(mDynamicAidGroups.values()));
436        }
437        dest.writeInt(mRequiresDeviceUnlock ? 1 : 0);
438        dest.writeInt(mBannerResourceId);
439        dest.writeInt(mUid);
440    };
441
442    public static final Parcelable.Creator<ApduServiceInfo> CREATOR =
443            new Parcelable.Creator<ApduServiceInfo>() {
444        @Override
445        public ApduServiceInfo createFromParcel(Parcel source) {
446            ResolveInfo info = ResolveInfo.CREATOR.createFromParcel(source);
447            String description = source.readString();
448            boolean onHost = (source.readInt() != 0) ? true : false;
449            ArrayList<AidGroup> staticAidGroups = new ArrayList<AidGroup>();
450            int numStaticGroups = source.readInt();
451            if (numStaticGroups > 0) {
452                source.readTypedList(staticAidGroups, AidGroup.CREATOR);
453            }
454            ArrayList<AidGroup> dynamicAidGroups = new ArrayList<AidGroup>();
455            int numDynamicGroups = source.readInt();
456            if (numDynamicGroups > 0) {
457                source.readTypedList(dynamicAidGroups, AidGroup.CREATOR);
458            }
459            boolean requiresUnlock = (source.readInt() != 0) ? true : false;
460            int bannerResource = source.readInt();
461            int uid = source.readInt();
462            return new ApduServiceInfo(info, onHost, description, staticAidGroups,
463                    dynamicAidGroups, requiresUnlock, bannerResource, uid);
464        }
465
466        @Override
467        public ApduServiceInfo[] newArray(int size) {
468            return new ApduServiceInfo[size];
469        }
470    };
471
472    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
473        pw.println("    " + getComponent() +
474                " (Description: " + getDescription() + ")");
475        pw.println("    Static AID groups:");
476        for (AidGroup group : mStaticAidGroups.values()) {
477            pw.println("        Category: " + group.category);
478            for (String aid : group.aids) {
479                pw.println("            AID: " + aid);
480            }
481        }
482        pw.println("    Dynamic AID groups:");
483        for (AidGroup group : mDynamicAidGroups.values()) {
484            pw.println("        Category: " + group.category);
485            for (String aid : group.aids) {
486                pw.println("            AID: " + aid);
487            }
488        }
489    }
490}
491