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 com.android.tv.parental; 18 19 import android.content.ContentUris; 20 import android.content.Context; 21 import android.content.pm.PackageManager.NameNotFoundException; 22 import android.content.res.Resources; 23 import android.content.res.XmlResourceParser; 24 import android.media.tv.TvContentRatingSystemInfo; 25 import android.net.Uri; 26 import android.util.Log; 27 28 import com.android.tv.parental.ContentRatingSystem.Order; 29 import com.android.tv.parental.ContentRatingSystem.Rating; 30 import com.android.tv.parental.ContentRatingSystem.SubRating; 31 32 import org.xmlpull.v1.XmlPullParser; 33 import org.xmlpull.v1.XmlPullParserException; 34 35 import java.io.IOException; 36 import java.util.ArrayList; 37 import java.util.List; 38 39 public class ContentRatingsParser { 40 private static final String TAG = "ContentRatingsParser"; 41 private static final boolean DEBUG = false; 42 43 public static final String DOMAIN_SYSTEM_RATINGS = "com.android.tv"; 44 45 private static final String TAG_RATING_SYSTEM_DEFINITIONS = "rating-system-definitions"; 46 private static final String TAG_RATING_SYSTEM_DEFINITION = "rating-system-definition"; 47 private static final String TAG_SUB_RATING_DEFINITION = "sub-rating-definition"; 48 private static final String TAG_RATING_DEFINITION = "rating-definition"; 49 private static final String TAG_SUB_RATING = "sub-rating"; 50 private static final String TAG_RATING = "rating"; 51 private static final String TAG_RATING_ORDER = "rating-order"; 52 53 private static final String ATTR_VERSION_CODE = "versionCode"; 54 private static final String ATTR_NAME = "name"; 55 private static final String ATTR_TITLE = "title"; 56 private static final String ATTR_COUNTRY = "country"; 57 private static final String ATTR_ICON = "icon"; 58 private static final String ATTR_DESCRIPTION = "description"; 59 private static final String ATTR_CONTENT_AGE_HINT = "contentAgeHint"; 60 private static final String VERSION_CODE = "1"; 61 62 private final Context mContext; 63 private Resources mResources; 64 private String mXmlVersionCode; 65 66 public ContentRatingsParser(Context context) { 67 mContext = context; 68 } 69 70 public List<ContentRatingSystem> parse(TvContentRatingSystemInfo info) { 71 List<ContentRatingSystem> ratingSystems = null; 72 Uri uri = info.getXmlUri(); 73 if (DEBUG) Log.d(TAG, "Parsing rating system for " + uri); 74 try { 75 String packageName = uri.getAuthority(); 76 int resId = (int) ContentUris.parseId(uri); 77 try (XmlResourceParser parser = mContext.getPackageManager() 78 .getXml(packageName, resId, null)) { 79 if (parser == null) { 80 throw new IllegalArgumentException("Cannot get XML with URI " + uri); 81 } 82 ratingSystems = parse(parser, packageName, !info.isSystemDefined()); 83 } 84 } catch (Exception e) { 85 // Catching all exceptions and print which URI is malformed XML with description 86 // and stack trace here. 87 // TODO: We may want to print message to stdout. 88 Log.w(TAG, "Error parsing XML " + uri, e); 89 } 90 return ratingSystems; 91 } 92 93 private List<ContentRatingSystem> parse(XmlResourceParser parser, String domain, 94 boolean isCustom) 95 throws XmlPullParserException, IOException { 96 try { 97 mResources = mContext.getPackageManager().getResourcesForApplication(domain); 98 } catch (NameNotFoundException e) { 99 Log.w(TAG, "Failed to get resources for " + domain, e); 100 mResources = mContext.getResources(); 101 } 102 // TODO: find another way to replace the domain the content rating systems defined in TV. 103 // Live TV app provides public content rating systems. Therefore, the domain of 104 // the content rating systems defined in TV app should be com.android.tv instead of 105 // this app's package name. 106 if (domain.equals(mContext.getPackageName())) { 107 domain = DOMAIN_SYSTEM_RATINGS; 108 } 109 110 // Consume all START_DOCUMENT which can appear more than once. 111 while (parser.next() == XmlPullParser.START_DOCUMENT) {} 112 113 int eventType = parser.getEventType(); 114 assertEquals(eventType, XmlPullParser.START_TAG, "Malformed XML: Not a valid XML file"); 115 assertEquals(parser.getName(), TAG_RATING_SYSTEM_DEFINITIONS, 116 "Malformed XML: Should start with tag " + TAG_RATING_SYSTEM_DEFINITIONS); 117 118 boolean hasVersionAttr = false; 119 for (int i = 0; i < parser.getAttributeCount(); i++) { 120 String attr = parser.getAttributeName(i); 121 if (ATTR_VERSION_CODE.equals(attr)) { 122 hasVersionAttr = true; 123 mXmlVersionCode = parser.getAttributeValue(i); 124 } 125 } 126 if (!hasVersionAttr) { 127 throw new XmlPullParserException("Malformed XML: Should contains a version attribute" 128 + " in " + TAG_RATING_SYSTEM_DEFINITIONS); 129 } 130 131 List<ContentRatingSystem> ratingSystems = new ArrayList<>(); 132 while (parser.next() != XmlPullParser.END_DOCUMENT) { 133 switch (parser.getEventType()) { 134 case XmlPullParser.START_TAG: 135 if (TAG_RATING_SYSTEM_DEFINITION.equals(parser.getName())) { 136 ratingSystems.add(parseRatingSystemDefinition(parser, domain, isCustom)); 137 } else { 138 checkVersion("Malformed XML: Should contains " + 139 TAG_RATING_SYSTEM_DEFINITION); 140 } 141 break; 142 case XmlPullParser.END_TAG: 143 if (TAG_RATING_SYSTEM_DEFINITIONS.equals(parser.getName())) { 144 eventType = parser.next(); 145 assertEquals(eventType, XmlPullParser.END_DOCUMENT, 146 "Malformed XML: Should end with tag " + 147 TAG_RATING_SYSTEM_DEFINITIONS); 148 return ratingSystems; 149 } else { 150 checkVersion("Malformed XML: Should end with tag " + 151 TAG_RATING_SYSTEM_DEFINITIONS); 152 } 153 } 154 } 155 throw new XmlPullParserException(TAG_RATING_SYSTEM_DEFINITIONS + 156 " section is incomplete or section ending tag is missing"); 157 } 158 159 private static void assertEquals(int a, int b, String msg) throws XmlPullParserException { 160 if (a != b) { 161 throw new XmlPullParserException(msg); 162 } 163 } 164 165 private static void assertEquals(String a, String b, String msg) throws XmlPullParserException { 166 if (!b.equals(a)) { 167 throw new XmlPullParserException(msg); 168 } 169 } 170 171 private void checkVersion(String msg) throws XmlPullParserException { 172 if (!VERSION_CODE.equals(mXmlVersionCode)) { 173 throw new XmlPullParserException(msg); 174 } 175 } 176 177 private ContentRatingSystem parseRatingSystemDefinition(XmlResourceParser parser, String domain, 178 boolean isCustom) throws XmlPullParserException, IOException { 179 ContentRatingSystem.Builder builder = new ContentRatingSystem.Builder(mContext); 180 181 builder.setDomain(domain); 182 for (int i = 0; i < parser.getAttributeCount(); i++) { 183 String attr = parser.getAttributeName(i); 184 switch (attr) { 185 case ATTR_NAME: 186 builder.setName(parser.getAttributeValue(i)); 187 break; 188 case ATTR_COUNTRY: 189 for (String country : parser.getAttributeValue(i).split("\\s*,\\s*")) { 190 builder.addCountry(country); 191 } 192 break; 193 case ATTR_TITLE: 194 builder.setTitle(getTitle(parser, i)); 195 break; 196 case ATTR_DESCRIPTION: 197 builder.setDescription( 198 mResources.getString(parser.getAttributeResourceValue(i, 0))); 199 break; 200 default: 201 checkVersion("Malformed XML: Unknown attribute " + attr + " in " + 202 TAG_RATING_SYSTEM_DEFINITION); 203 } 204 } 205 206 while (parser.next() != XmlPullParser.END_DOCUMENT) { 207 switch (parser.getEventType()) { 208 case XmlPullParser.START_TAG: 209 String tag = parser.getName(); 210 switch (tag) { 211 case TAG_RATING_DEFINITION: 212 builder.addRatingBuilder(parseRatingDefinition(parser)); 213 break; 214 case TAG_SUB_RATING_DEFINITION: 215 builder.addSubRatingBuilder(parseSubRatingDefinition(parser)); 216 break; 217 case TAG_RATING_ORDER: 218 builder.addOrderBuilder(parseOrder(parser)); 219 break; 220 default: 221 checkVersion("Malformed XML: Unknown tag " + tag + " in " + 222 TAG_RATING_SYSTEM_DEFINITION); 223 } 224 break; 225 case XmlPullParser.END_TAG: 226 if (TAG_RATING_SYSTEM_DEFINITION.equals(parser.getName())) { 227 builder.setIsCustom(isCustom); 228 return builder.build(); 229 } else { 230 checkVersion("Malformed XML: Tag mismatch for " + 231 TAG_RATING_SYSTEM_DEFINITION); 232 } 233 } 234 } 235 throw new XmlPullParserException(TAG_RATING_SYSTEM_DEFINITION + 236 " section is incomplete or section ending tag is missing"); 237 } 238 239 private Rating.Builder parseRatingDefinition(XmlResourceParser parser) 240 throws XmlPullParserException, IOException { 241 Rating.Builder builder = new Rating.Builder(); 242 243 for (int i = 0; i < parser.getAttributeCount(); i++) { 244 String attr = parser.getAttributeName(i); 245 switch (attr) { 246 case ATTR_NAME: 247 builder.setName(parser.getAttributeValue(i)); 248 break; 249 case ATTR_TITLE: 250 builder.setTitle(getTitle(parser, i)); 251 break; 252 case ATTR_DESCRIPTION: 253 builder.setDescription( 254 mResources.getString(parser.getAttributeResourceValue(i, 0))); 255 break; 256 case ATTR_ICON: 257 builder.setIcon( 258 mResources.getDrawable(parser.getAttributeResourceValue(i, 0), null)); 259 break; 260 case ATTR_CONTENT_AGE_HINT: 261 int contentAgeHint = -1; 262 try { 263 contentAgeHint = Integer.parseInt(parser.getAttributeValue(i)); 264 } catch (NumberFormatException ignored) { 265 } 266 267 if (contentAgeHint < 0) { 268 throw new XmlPullParserException("Malformed XML: " + ATTR_CONTENT_AGE_HINT + 269 " should be a non-negative number"); 270 } 271 builder.setContentAgeHint(contentAgeHint); 272 break; 273 default: 274 checkVersion("Malformed XML: Unknown attribute " + attr + " in " + 275 TAG_RATING_DEFINITION); 276 } 277 } 278 279 while (parser.next() != XmlPullParser.END_DOCUMENT) { 280 switch (parser.getEventType()) { 281 case XmlPullParser.START_TAG: 282 if (TAG_SUB_RATING.equals(parser.getName())) { 283 builder = parseSubRating(parser, builder); 284 } else { 285 checkVersion(("Malformed XML: Only " + TAG_SUB_RATING + " is allowed in " + 286 TAG_RATING_DEFINITION)); 287 } 288 break; 289 case XmlPullParser.END_TAG: 290 if (TAG_RATING_DEFINITION.equals(parser.getName())) { 291 return builder; 292 } else { 293 checkVersion("Malformed XML: Tag mismatch for " + TAG_RATING_DEFINITION); 294 } 295 } 296 } 297 throw new XmlPullParserException(TAG_RATING_DEFINITION + 298 " section is incomplete or section ending tag is missing"); 299 } 300 301 private SubRating.Builder parseSubRatingDefinition(XmlResourceParser parser) 302 throws XmlPullParserException, IOException { 303 SubRating.Builder builder = new SubRating.Builder(); 304 305 for (int i = 0; i < parser.getAttributeCount(); i++) { 306 String attr = parser.getAttributeName(i); 307 switch (attr) { 308 case ATTR_NAME: 309 builder.setName(parser.getAttributeValue(i)); 310 break; 311 case ATTR_TITLE: 312 builder.setTitle(getTitle(parser, i)); 313 break; 314 case ATTR_DESCRIPTION: 315 builder.setDescription( 316 mResources.getString(parser.getAttributeResourceValue(i, 0))); 317 break; 318 case ATTR_ICON: 319 builder.setIcon( 320 mResources.getDrawable(parser.getAttributeResourceValue(i, 0), null)); 321 break; 322 default: 323 checkVersion("Malformed XML: Unknown attribute " + attr + " in " + 324 TAG_SUB_RATING_DEFINITION); 325 } 326 } 327 328 while (parser.next() != XmlPullParser.END_DOCUMENT) { 329 switch (parser.getEventType()) { 330 case XmlPullParser.END_TAG: 331 if (TAG_SUB_RATING_DEFINITION.equals(parser.getName())) { 332 return builder; 333 } else { 334 checkVersion("Malformed XML: " + TAG_SUB_RATING_DEFINITION + 335 " isn't closed"); 336 } 337 break; 338 default: 339 checkVersion("Malformed XML: " + TAG_SUB_RATING_DEFINITION + " has child"); 340 } 341 } 342 throw new XmlPullParserException(TAG_SUB_RATING_DEFINITION + 343 " section is incomplete or section ending tag is missing"); 344 } 345 346 private Order.Builder parseOrder(XmlResourceParser parser) 347 throws XmlPullParserException, IOException { 348 Order.Builder builder = new Order.Builder(); 349 350 assertEquals(parser.getAttributeCount(), 0, 351 "Malformed XML: Attribute isn't allowed in " + TAG_RATING_ORDER); 352 353 while (parser.next() != XmlPullParser.END_DOCUMENT) { 354 switch (parser.getEventType()) { 355 case XmlPullParser.START_TAG: 356 if (TAG_RATING.equals(parser.getName())) { 357 builder = parseRating(parser, builder); 358 } else { 359 checkVersion("Malformed XML: Only " + TAG_RATING + " is allowed in " + 360 TAG_RATING_ORDER); 361 } 362 break; 363 case XmlPullParser.END_TAG: 364 assertEquals(parser.getName(), TAG_RATING_ORDER, 365 "Malformed XML: Tag mismatch for " + TAG_RATING_ORDER); 366 return builder; 367 } 368 } 369 throw new XmlPullParserException(TAG_RATING_ORDER + 370 " section is incomplete or section ending tag is missing"); 371 } 372 373 private Order.Builder parseRating(XmlResourceParser parser, Order.Builder builder) 374 throws XmlPullParserException, IOException { 375 for (int i = 0; i < parser.getAttributeCount(); i++) { 376 String attr = parser.getAttributeName(i); 377 switch (attr) { 378 case ATTR_NAME: 379 builder.addRatingName(parser.getAttributeValue(i)); 380 break; 381 default: 382 checkVersion("Malformed XML: " + TAG_RATING_ORDER + " should only contain " 383 + ATTR_NAME); 384 } 385 } 386 387 while (parser.next() != XmlPullParser.END_DOCUMENT) { 388 if (parser.getEventType() == XmlPullParser.END_TAG) { 389 if (TAG_RATING.equals(parser.getName())) { 390 return builder; 391 } else { 392 checkVersion("Malformed XML: " + TAG_RATING + " has child"); 393 } 394 } 395 } 396 throw new XmlPullParserException(TAG_RATING + 397 " section is incomplete or section ending tag is missing"); 398 } 399 400 private Rating.Builder parseSubRating(XmlResourceParser parser, Rating.Builder builder) 401 throws XmlPullParserException, IOException { 402 for (int i = 0; i < parser.getAttributeCount(); i++) { 403 String attr = parser.getAttributeName(i); 404 switch (attr) { 405 case ATTR_NAME: 406 builder.addSubRatingName(parser.getAttributeValue(i)); 407 break; 408 default: 409 checkVersion("Malformed XML: " + TAG_SUB_RATING + " should only contain " + 410 ATTR_NAME); 411 } 412 } 413 414 while (parser.next() != XmlPullParser.END_DOCUMENT) { 415 if (parser.getEventType() == XmlPullParser.END_TAG) { 416 if (TAG_SUB_RATING.equals(parser.getName())) { 417 return builder; 418 } else { 419 checkVersion("Malformed XML: " + TAG_SUB_RATING + " has child"); 420 } 421 } 422 } 423 throw new XmlPullParserException(TAG_SUB_RATING + 424 " section is incomplete or section ending tag is missing"); 425 } 426 427 // Title might be a resource id or a string value. Try loading as an id first, then use the 428 // string if that fails. 429 private String getTitle(XmlResourceParser parser, int index) { 430 int titleResId = parser.getAttributeResourceValue(index, 0); 431 if (titleResId != 0) { 432 return mResources.getString(titleResId); 433 } 434 return parser.getAttributeValue(index); 435 } 436 } 437