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