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 
     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