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