Home | History | Annotate | Download | only in parental
      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 import com.android.tv.parental.ContentRatingSystem.Order;
     28 import com.android.tv.parental.ContentRatingSystem.Rating;
     29 import com.android.tv.parental.ContentRatingSystem.SubRating;
     30 import java.io.IOException;
     31 import java.util.ArrayList;
     32 import java.util.List;
     33 import org.xmlpull.v1.XmlPullParser;
     34 import org.xmlpull.v1.XmlPullParserException;
     35 
     36 /** Parses Content Ratings */
     37 public 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