• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
ContentRatingsParser(Context context)64     public ContentRatingsParser(Context context) {
65         mContext = context;
66     }
67 
parse(TvContentRatingSystemInfo info)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 
parse( XmlResourceParser parser, String domain, boolean isCustom)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 
assertEquals(int a, int b, String msg)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 
assertEquals(String a, String b, String msg)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 
checkVersion(String msg)177     private void checkVersion(String msg) throws XmlPullParserException {
178         if (!VERSION_CODE.equals(mXmlVersionCode)) {
179             throw new XmlPullParserException(msg);
180         }
181     }
182 
parseRatingSystemDefinition( XmlResourceParser parser, String domain, boolean isCustom)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 
parseRatingDefinition(XmlResourceParser parser)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 
parseSubRatingDefinition(XmlResourceParser parser)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 
parseOrder(XmlResourceParser parser)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 
parseRating(XmlResourceParser parser, Order.Builder builder)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 
parseSubRating(XmlResourceParser parser, Rating.Builder builder)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.
getTitle(XmlResourceParser parser, int index)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