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