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