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 (CardEmulation.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                } else if (eventType == XmlPullParser.START_TAG &&
228                        "aid-prefix-filter".equals(tagName) && currentGroup != null) {
229                    final TypedArray a = res.obtainAttributes(attrs,
230                            com.android.internal.R.styleable.AidFilter);
231                    String aid = a.getString(com.android.internal.R.styleable.AidFilter_name).
232                            toUpperCase();
233                    // Add wildcard char to indicate prefix
234                    aid = aid.concat("*");
235                    if (CardEmulation.isValidAid(aid) && !currentGroup.aids.contains(aid)) {
236                        currentGroup.aids.add(aid);
237                    } else {
238                        Log.e(TAG, "Ignoring invalid or duplicate aid: " + aid);
239                    }
240                    a.recycle();
241                }
242            }
243        } catch (NameNotFoundException e) {
244            throw new XmlPullParserException("Unable to create context for: " + si.packageName);
245        } finally {
246            if (parser != null) parser.close();
247        }
248        // Set uid
249        mUid = si.applicationInfo.uid;
250    }
251
252    public ComponentName getComponent() {
253        return new ComponentName(mService.serviceInfo.packageName,
254                mService.serviceInfo.name);
255    }
256
257    /**
258     * Returns a consolidated list of AIDs from the AID groups
259     * registered by this service. Note that if a service has both
260     * a static (manifest-based) AID group for a category and a dynamic
261     * AID group, only the dynamically registered AIDs will be returned
262     * for that category.
263     * @return List of AIDs registered by the service
264     */
265    public ArrayList<String> getAids() {
266        final ArrayList<String> aids = new ArrayList<String>();
267        for (AidGroup group : getAidGroups()) {
268            aids.addAll(group.aids);
269        }
270        return aids;
271    }
272
273    /**
274     * Returns the registered AID group for this category.
275     */
276    public AidGroup getDynamicAidGroupForCategory(String category) {
277        return mDynamicAidGroups.get(category);
278    }
279
280    public boolean removeDynamicAidGroupForCategory(String category) {
281        return (mDynamicAidGroups.remove(category) != null);
282    }
283
284    /**
285     * Returns a consolidated list of AID groups
286     * registered by this service. Note that if a service has both
287     * a static (manifest-based) AID group for a category and a dynamic
288     * AID group, only the dynamically registered AID group will be returned
289     * for that category.
290     * @return List of AIDs registered by the service
291     */
292    public ArrayList<AidGroup> getAidGroups() {
293        final ArrayList<AidGroup> groups = new ArrayList<AidGroup>();
294        for (Map.Entry<String, AidGroup> entry : mDynamicAidGroups.entrySet()) {
295            groups.add(entry.getValue());
296        }
297        for (Map.Entry<String, AidGroup> entry : mStaticAidGroups.entrySet()) {
298            if (!mDynamicAidGroups.containsKey(entry.getKey())) {
299                // Consolidate AID groups - don't return static ones
300                // if a dynamic group exists for the category.
301                groups.add(entry.getValue());
302            }
303        }
304        return groups;
305    }
306
307    /**
308     * Returns the category to which this service has attributed the AID that is passed in,
309     * or null if we don't know this AID.
310     */
311    public String getCategoryForAid(String aid) {
312        ArrayList<AidGroup> groups = getAidGroups();
313        for (AidGroup group : groups) {
314            if (group.aids.contains(aid.toUpperCase())) {
315                return group.category;
316            }
317        }
318        return null;
319    }
320
321    public boolean hasCategory(String category) {
322        return (mStaticAidGroups.containsKey(category) || mDynamicAidGroups.containsKey(category));
323    }
324
325    public boolean isOnHost() {
326        return mOnHost;
327    }
328
329    public boolean requiresUnlock() {
330        return mRequiresDeviceUnlock;
331    }
332
333    public String getDescription() {
334        return mDescription;
335    }
336
337    public int getUid() {
338        return mUid;
339    }
340
341    public void setOrReplaceDynamicAidGroup(AidGroup aidGroup) {
342        mDynamicAidGroups.put(aidGroup.getCategory(), aidGroup);
343    }
344
345    public CharSequence loadLabel(PackageManager pm) {
346        return mService.loadLabel(pm);
347    }
348
349    public Drawable loadIcon(PackageManager pm) {
350        return mService.loadIcon(pm);
351    }
352
353    public Drawable loadBanner(PackageManager pm) {
354        Resources res;
355        try {
356            res = pm.getResourcesForApplication(mService.serviceInfo.packageName);
357            Drawable banner = res.getDrawable(mBannerResourceId);
358            return banner;
359        } catch (NotFoundException e) {
360            Log.e(TAG, "Could not load banner.");
361            return null;
362        } catch (NameNotFoundException e) {
363            Log.e(TAG, "Could not load banner.");
364            return null;
365        }
366    }
367
368    @Override
369    public String toString() {
370        StringBuilder out = new StringBuilder("ApduService: ");
371        out.append(getComponent());
372        out.append(", description: " + mDescription);
373        out.append(", Static AID Groups: ");
374        for (AidGroup aidGroup : mStaticAidGroups.values()) {
375            out.append(aidGroup.toString());
376        }
377        out.append(", Dynamic AID Groups: ");
378        for (AidGroup aidGroup : mDynamicAidGroups.values()) {
379            out.append(aidGroup.toString());
380        }
381        return out.toString();
382    }
383
384    @Override
385    public boolean equals(Object o) {
386        if (this == o) return true;
387        if (!(o instanceof ApduServiceInfo)) return false;
388        ApduServiceInfo thatService = (ApduServiceInfo) o;
389
390        return thatService.getComponent().equals(this.getComponent());
391    }
392
393    @Override
394    public int hashCode() {
395        return getComponent().hashCode();
396    }
397
398
399    @Override
400    public int describeContents() {
401        return 0;
402    }
403
404    @Override
405    public void writeToParcel(Parcel dest, int flags) {
406        mService.writeToParcel(dest, flags);
407        dest.writeString(mDescription);
408        dest.writeInt(mOnHost ? 1 : 0);
409        dest.writeInt(mStaticAidGroups.size());
410        if (mStaticAidGroups.size() > 0) {
411            dest.writeTypedList(new ArrayList<AidGroup>(mStaticAidGroups.values()));
412        }
413        dest.writeInt(mDynamicAidGroups.size());
414        if (mDynamicAidGroups.size() > 0) {
415            dest.writeTypedList(new ArrayList<AidGroup>(mDynamicAidGroups.values()));
416        }
417        dest.writeInt(mRequiresDeviceUnlock ? 1 : 0);
418        dest.writeInt(mBannerResourceId);
419        dest.writeInt(mUid);
420    };
421
422    public static final Parcelable.Creator<ApduServiceInfo> CREATOR =
423            new Parcelable.Creator<ApduServiceInfo>() {
424        @Override
425        public ApduServiceInfo createFromParcel(Parcel source) {
426            ResolveInfo info = ResolveInfo.CREATOR.createFromParcel(source);
427            String description = source.readString();
428            boolean onHost = source.readInt() != 0;
429            ArrayList<AidGroup> staticAidGroups = new ArrayList<AidGroup>();
430            int numStaticGroups = source.readInt();
431            if (numStaticGroups > 0) {
432                source.readTypedList(staticAidGroups, AidGroup.CREATOR);
433            }
434            ArrayList<AidGroup> dynamicAidGroups = new ArrayList<AidGroup>();
435            int numDynamicGroups = source.readInt();
436            if (numDynamicGroups > 0) {
437                source.readTypedList(dynamicAidGroups, AidGroup.CREATOR);
438            }
439            boolean requiresUnlock = source.readInt() != 0;
440            int bannerResource = source.readInt();
441            int uid = source.readInt();
442            return new ApduServiceInfo(info, onHost, description, staticAidGroups,
443                    dynamicAidGroups, requiresUnlock, bannerResource, uid);
444        }
445
446        @Override
447        public ApduServiceInfo[] newArray(int size) {
448            return new ApduServiceInfo[size];
449        }
450    };
451
452    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
453        pw.println("    " + getComponent() +
454                " (Description: " + getDescription() + ")");
455        pw.println("    Static AID groups:");
456        for (AidGroup group : mStaticAidGroups.values()) {
457            pw.println("        Category: " + group.category);
458            for (String aid : group.aids) {
459                pw.println("            AID: " + aid);
460            }
461        }
462        pw.println("    Dynamic AID groups:");
463        for (AidGroup group : mDynamicAidGroups.values()) {
464            pw.println("        Category: " + group.category);
465            for (String aid : group.aids) {
466                pw.println("            AID: " + aid);
467            }
468        }
469    }
470}
471