1 /* 2 * Copyright (C) 2015 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.annotation.FlaggedApi; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.SystemApi; 23 import android.nfc.Flags; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 import android.util.Log; 27 import android.util.proto.ProtoOutputStream; 28 29 import org.xmlpull.v1.XmlPullParser; 30 import org.xmlpull.v1.XmlPullParserException; 31 import org.xmlpull.v1.XmlSerializer; 32 33 import java.io.IOException; 34 import java.util.ArrayList; 35 import java.util.List; 36 import java.util.Locale; 37 import java.util.regex.Pattern; 38 39 /********************************************************************** 40 * This file is not a part of the NFC mainline module * 41 * *******************************************************************/ 42 43 /** 44 * The AidGroup class represents a group of Application Identifiers (AIDs). 45 * 46 * <p>The format of AIDs is defined in the ISO/IEC 7816-4 specification. This class 47 * requires the AIDs to be input as a hexadecimal string, with an even amount of 48 * hexadecimal characters, e.g. "F014811481". 49 * 50 * @hide 51 */ 52 @SystemApi 53 @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) 54 public final class AidGroup implements Parcelable { 55 /** 56 * The maximum number of AIDs that can be present in any one group. 57 */ 58 private static final int MAX_NUM_AIDS = 256; 59 60 private static final String TAG = "AidGroup"; 61 62 63 private final List<String> mAids; 64 private final String mCategory; 65 @SuppressWarnings("unused") // Unused as of now, but part of the XML input. 66 private final String mDescription; 67 68 /** 69 * Creates a new AidGroup object. 70 * 71 * @param aids list of AIDs present in the group 72 * @param category category of this group, e.g. {@link CardEmulation#CATEGORY_PAYMENT} 73 */ 74 @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) AidGroup(@onNull List<String> aids, @Nullable String category)75 public AidGroup(@NonNull List<String> aids, @Nullable String category) { 76 if (aids == null || aids.size() == 0) { 77 throw new IllegalArgumentException("No AIDS in AID group."); 78 } 79 if (aids.size() > MAX_NUM_AIDS) { 80 throw new IllegalArgumentException("Too many AIDs in AID group."); 81 } 82 for (String aid : aids) { 83 if (!isValidAid(aid)) { 84 throw new IllegalArgumentException("AID " + aid + " is not a valid AID."); 85 } 86 } 87 if (isValidCategory(category)) { 88 this.mCategory = category; 89 } else { 90 this.mCategory = CardEmulation.CATEGORY_OTHER; 91 } 92 this.mAids = new ArrayList<String>(aids.size()); 93 for (String aid : aids) { 94 this.mAids.add(aid.toUpperCase(Locale.US)); 95 } 96 this.mDescription = null; 97 } 98 99 /** 100 * Creates a new AidGroup object. 101 * 102 * @param category category of this group, e.g. {@link CardEmulation#CATEGORY_PAYMENT} 103 * @param description description of this group 104 */ AidGroup(@onNull String category, @NonNull String description)105 AidGroup(@NonNull String category, @NonNull String description) { 106 this.mAids = new ArrayList<String>(); 107 this.mCategory = category; 108 this.mDescription = description; 109 } 110 111 /** 112 * Returns the category of this group. 113 * @return the category of this AID group 114 */ 115 @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) 116 @NonNull getCategory()117 public String getCategory() { 118 return mCategory; 119 } 120 121 /** 122 * Returns the list of AIDs in this group. 123 * 124 * @return the list of AIDs in this group 125 */ 126 @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) 127 @NonNull getAids()128 public List<String> getAids() { 129 return mAids; 130 } 131 132 @Override toString()133 public String toString() { 134 StringBuilder out = new StringBuilder("Category: " + mCategory 135 + ", AIDs:"); 136 for (String aid : mAids) { 137 out.append(aid); 138 out.append(", "); 139 } 140 return out.toString(); 141 } 142 143 /** 144 * Dump debugging info as AidGroupProto. 145 * 146 * If the output belongs to a sub message, the caller is responsible for wrapping this function 147 * between {@link ProtoOutputStream#start(long)} and {@link ProtoOutputStream#end(long)}. 148 * 149 * @param proto the ProtoOutputStream to write to 150 */ 151 @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) dump(@onNull ProtoOutputStream proto)152 public void dump(@NonNull ProtoOutputStream proto) { 153 proto.write(AidGroupProto.CATEGORY, mCategory); 154 for (String aid : mAids) { 155 proto.write(AidGroupProto.AIDS, aid); 156 } 157 } 158 159 @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) 160 @Override describeContents()161 public int describeContents() { 162 return 0; 163 } 164 165 @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) 166 @Override writeToParcel(@onNull Parcel dest, int flags)167 public void writeToParcel(@NonNull Parcel dest, int flags) { 168 dest.writeString8(mCategory); 169 dest.writeInt(mAids.size()); 170 if (mAids.size() > 0) { 171 dest.writeStringList(mAids); 172 } 173 } 174 175 @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) 176 public static final @NonNull Parcelable.Creator<AidGroup> CREATOR = 177 new Parcelable.Creator<AidGroup>() { 178 179 @Override 180 public AidGroup createFromParcel(Parcel source) { 181 String category = source.readString8(); 182 int listSize = source.readInt(); 183 ArrayList<String> aidList = new ArrayList<String>(); 184 if (listSize > 0) { 185 source.readStringList(aidList); 186 } 187 return new AidGroup(aidList, category); 188 } 189 190 @Override 191 public AidGroup[] newArray(int size) { 192 return new AidGroup[size]; 193 } 194 }; 195 196 /** 197 * Create an instance of AID group from XML file. 198 * 199 * @param parser input xml parser stream 200 * @throws XmlPullParserException If an error occurs parsing the element. 201 * @throws IOException If an error occurs reading the element. 202 */ 203 @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) 204 @Nullable createFromXml(@onNull XmlPullParser parser)205 public static AidGroup createFromXml(@NonNull XmlPullParser parser) 206 throws XmlPullParserException, IOException { 207 String category = null; 208 ArrayList<String> aids = new ArrayList<String>(); 209 AidGroup group = null; 210 boolean inGroup = false; 211 212 int eventType = parser.getEventType(); 213 int minDepth = parser.getDepth(); 214 while (eventType != XmlPullParser.END_DOCUMENT && parser.getDepth() >= minDepth) { 215 String tagName = parser.getName(); 216 if (eventType == XmlPullParser.START_TAG) { 217 if (tagName.equals("aid")) { 218 if (inGroup) { 219 String aid = parser.getAttributeValue(null, "value"); 220 if (aid != null) { 221 aids.add(aid.toUpperCase()); 222 } 223 } else { 224 Log.d(TAG, "Ignoring <aid> tag while not in group"); 225 } 226 } else if (tagName.equals("aid-group")) { 227 category = parser.getAttributeValue(null, "category"); 228 if (category == null) { 229 Log.e(TAG, "<aid-group> tag without valid category"); 230 return null; 231 } 232 inGroup = true; 233 } else { 234 Log.d(TAG, "Ignoring unexpected tag: " + tagName); 235 } 236 } else if (eventType == XmlPullParser.END_TAG) { 237 if (tagName.equals("aid-group") && inGroup && aids.size() > 0) { 238 group = new AidGroup(aids, category); 239 break; 240 } 241 } 242 eventType = parser.next(); 243 } 244 return group; 245 } 246 247 /** 248 * Serialize instance of AID group to XML file. 249 * @param out XML serializer stream 250 * @throws IOException If an error occurs reading the element. 251 */ 252 @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) writeAsXml(@onNull XmlSerializer out)253 public void writeAsXml(@NonNull XmlSerializer out) throws IOException { 254 out.startTag(null, "aid-group"); 255 out.attribute(null, "category", mCategory); 256 for (String aid : mAids) { 257 out.startTag(null, "aid"); 258 out.attribute(null, "value", aid); 259 out.endTag(null, "aid"); 260 } 261 out.endTag(null, "aid-group"); 262 } 263 isValidCategory(String category)264 private static boolean isValidCategory(String category) { 265 return CardEmulation.CATEGORY_PAYMENT.equals(category) || 266 CardEmulation.CATEGORY_OTHER.equals(category); 267 } 268 269 private static final Pattern AID_PATTERN = Pattern.compile("[0-9A-Fa-f]{10,32}\\*?\\#?"); 270 /** 271 * Copied over from {@link CardEmulation#isValidAid(String)} 272 * @hide 273 */ isValidAid(String aid)274 private static boolean isValidAid(String aid) { 275 if (aid == null) 276 return false; 277 278 // If a prefix/subset AID, the total length must be odd (even # of AID chars + '*') 279 if ((aid.endsWith("*") || aid.endsWith("#")) && ((aid.length() % 2) == 0)) { 280 Log.e(TAG, "AID " + aid + " is not a valid AID."); 281 return false; 282 } 283 284 // If not a prefix/subset AID, the total length must be even (even # of AID chars) 285 if ((!(aid.endsWith("*") || aid.endsWith("#"))) && ((aid.length() % 2) != 0)) { 286 Log.e(TAG, "AID " + aid + " is not a valid AID."); 287 return false; 288 } 289 290 // Verify hex characters 291 if (!AID_PATTERN.matcher(aid).matches()) { 292 Log.e(TAG, "AID " + aid + " is not a valid AID."); 293 return false; 294 } 295 296 return true; 297 } 298 } 299