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