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 
17 package android.nfc.cardemulation;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.pm.PackageManager;
22 import android.content.pm.ResolveInfo;
23 import android.content.pm.ServiceInfo;
24 import android.content.pm.PackageManager.NameNotFoundException;
25 import android.content.res.Resources;
26 import android.content.res.Resources.NotFoundException;
27 import android.content.res.TypedArray;
28 import android.content.res.XmlResourceParser;
29 import android.graphics.drawable.Drawable;
30 import android.os.Parcel;
31 import android.os.Parcelable;
32 import android.os.ResultReceiver;
33 import android.util.AttributeSet;
34 import android.util.Log;
35 import android.util.Xml;
36 
37 import org.xmlpull.v1.XmlPullParser;
38 import org.xmlpull.v1.XmlPullParserException;
39 
40 import java.io.FileDescriptor;
41 import java.io.IOException;
42 import java.io.PrintWriter;
43 import java.util.ArrayList;
44 import java.util.HashMap;
45 import java.util.List;
46 import java.util.Map;
47 
48 /**
49  * @hide
50  */
51 public 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      */
ApduServiceInfo(ResolveInfo info, boolean onHost, String description, ArrayList<AidGroup> staticAidGroups, ArrayList<AidGroup> dynamicAidGroups, boolean requiresUnlock, int bannerResource, int uid, String settingsActivityName)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 
ApduServiceInfo(PackageManager pm, ResolveInfo info, boolean onHost)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 
getComponent()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      */
getAids()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 
getPrefixAids()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      */
getDynamicAidGroupForCategory(String category)303     public AidGroup getDynamicAidGroupForCategory(String category) {
304         return mDynamicAidGroups.get(category);
305     }
306 
removeDynamicAidGroupForCategory(String category)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      */
getAidGroups()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      */
getCategoryForAid(String aid)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 
hasCategory(String category)348     public boolean hasCategory(String category) {
349         return (mStaticAidGroups.containsKey(category) || mDynamicAidGroups.containsKey(category));
350     }
351 
isOnHost()352     public boolean isOnHost() {
353         return mOnHost;
354     }
355 
requiresUnlock()356     public boolean requiresUnlock() {
357         return mRequiresDeviceUnlock;
358     }
359 
getDescription()360     public String getDescription() {
361         return mDescription;
362     }
363 
getUid()364     public int getUid() {
365         return mUid;
366     }
367 
setOrReplaceDynamicAidGroup(AidGroup aidGroup)368     public void setOrReplaceDynamicAidGroup(AidGroup aidGroup) {
369         mDynamicAidGroups.put(aidGroup.getCategory(), aidGroup);
370     }
371 
loadLabel(PackageManager pm)372     public CharSequence loadLabel(PackageManager pm) {
373         return mService.loadLabel(pm);
374     }
375 
loadAppLabel(PackageManager pm)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 
loadIcon(PackageManager pm)385     public Drawable loadIcon(PackageManager pm) {
386         return mService.loadIcon(pm);
387     }
388 
loadBanner(PackageManager pm)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 
getSettingsActivityName()404     public String getSettingsActivityName() { return mSettingsActivityName; }
405 
406     @Override
toString()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
equals(Object o)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
hashCode()432     public int hashCode() {
433         return getComponent().hashCode();
434     }
435 
436 
437     @Override
describeContents()438     public int describeContents() {
439         return 0;
440     }
441 
442     @Override
writeToParcel(Parcel dest, int flags)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 
dump(FileDescriptor fd, PrintWriter pw, String[] args)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