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