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 
ContentRatingsParser(Context context)66     public ContentRatingsParser(Context context) {
67         mContext = context;
68     }
69 
parse(TvContentRatingSystemInfo info)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 
parse(XmlResourceParser parser, String domain, boolean isCustom)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 
assertEquals(int a, int b, String msg)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 
assertEquals(String a, String b, String msg)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 
checkVersion(String msg)171     private void checkVersion(String msg) throws XmlPullParserException {
172         if (!VERSION_CODE.equals(mXmlVersionCode)) {
173             throw new XmlPullParserException(msg);
174         }
175     }
176 
parseRatingSystemDefinition(XmlResourceParser parser, String domain, boolean isCustom)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 
parseRatingDefinition(XmlResourceParser parser)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 
parseSubRatingDefinition(XmlResourceParser parser)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 
parseOrder(XmlResourceParser parser)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 
parseRating(XmlResourceParser parser, Order.Builder builder)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 
parseSubRating(XmlResourceParser parser, Rating.Builder builder)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.
getTitle(XmlResourceParser parser, int index)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