blob: 14df88ea923464e16f64d282de9996a473c2eb5c [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.parental;
import android.content.ContentUris;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.media.tv.TvContentRatingSystemInfo;
import android.net.Uri;
import android.util.Log;
import com.android.tv.parental.ContentRatingSystem.Order;
import com.android.tv.parental.ContentRatingSystem.Rating;
import com.android.tv.parental.ContentRatingSystem.SubRating;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
/** Parses Content Ratings */
public class ContentRatingsParser {
private static final String TAG = "ContentRatingsParser";
private static final boolean DEBUG = false;
public static final String DOMAIN_SYSTEM_RATINGS = "com.android.tv";
private static final String TAG_RATING_SYSTEM_DEFINITIONS = "rating-system-definitions";
private static final String TAG_RATING_SYSTEM_DEFINITION = "rating-system-definition";
private static final String TAG_SUB_RATING_DEFINITION = "sub-rating-definition";
private static final String TAG_RATING_DEFINITION = "rating-definition";
private static final String TAG_SUB_RATING = "sub-rating";
private static final String TAG_RATING = "rating";
private static final String TAG_RATING_ORDER = "rating-order";
private static final String ATTR_VERSION_CODE = "versionCode";
private static final String ATTR_NAME = "name";
private static final String ATTR_TITLE = "title";
private static final String ATTR_COUNTRY = "country";
private static final String ATTR_ICON = "icon";
private static final String ATTR_DESCRIPTION = "description";
private static final String ATTR_CONTENT_AGE_HINT = "contentAgeHint";
private static final String VERSION_CODE = "1";
private final Context mContext;
private Resources mResources;
private String mXmlVersionCode;
public ContentRatingsParser(Context context) {
mContext = context;
}
public List<ContentRatingSystem> parse(TvContentRatingSystemInfo info) {
List<ContentRatingSystem> ratingSystems = null;
Uri uri = info.getXmlUri();
if (DEBUG) Log.d(TAG, "Parsing rating system for " + uri);
try {
String packageName = uri.getAuthority();
int resId = (int) ContentUris.parseId(uri);
try (XmlResourceParser parser =
mContext.getPackageManager().getXml(packageName, resId, null)) {
if (parser == null) {
throw new IllegalArgumentException("Cannot get XML with URI " + uri);
}
ratingSystems = parse(parser, packageName, !info.isSystemDefined());
}
} catch (Exception e) {
// Catching all exceptions and print which URI is malformed XML with description
// and stack trace here.
// TODO: We may want to print message to stdout.
Log.w(TAG, "Error parsing XML " + uri, e);
}
return ratingSystems;
}
private List<ContentRatingSystem> parse(
XmlResourceParser parser, String domain, boolean isCustom)
throws XmlPullParserException, IOException {
try {
mResources = mContext.getPackageManager().getResourcesForApplication(domain);
} catch (NameNotFoundException e) {
Log.w(TAG, "Failed to get resources for " + domain, e);
mResources = mContext.getResources();
}
// TODO: find another way to replace the domain the content rating systems defined in TV.
// Live TV app provides public content rating systems. Therefore, the domain of
// the content rating systems defined in TV app should be com.android.tv instead of
// this app's package name.
if (domain.equals(mContext.getPackageName())) {
domain = DOMAIN_SYSTEM_RATINGS;
}
// Consume all START_DOCUMENT which can appear more than once.
while (parser.next() == XmlPullParser.START_DOCUMENT) {}
int eventType = parser.getEventType();
assertEquals(eventType, XmlPullParser.START_TAG, "Malformed XML: Not a valid XML file");
assertEquals(
parser.getName(),
TAG_RATING_SYSTEM_DEFINITIONS,
"Malformed XML: Should start with tag " + TAG_RATING_SYSTEM_DEFINITIONS);
boolean hasVersionAttr = false;
for (int i = 0; i < parser.getAttributeCount(); i++) {
String attr = parser.getAttributeName(i);
if (ATTR_VERSION_CODE.equals(attr)) {
hasVersionAttr = true;
mXmlVersionCode = parser.getAttributeValue(i);
}
}
if (!hasVersionAttr) {
throw new XmlPullParserException(
"Malformed XML: Should contains a version attribute"
+ " in "
+ TAG_RATING_SYSTEM_DEFINITIONS);
}
List<ContentRatingSystem> ratingSystems = new ArrayList<>();
while (parser.next() != XmlPullParser.END_DOCUMENT) {
switch (parser.getEventType()) {
case XmlPullParser.START_TAG:
if (TAG_RATING_SYSTEM_DEFINITION.equals(parser.getName())) {
ratingSystems.add(parseRatingSystemDefinition(parser, domain, isCustom));
} else {
checkVersion(
"Malformed XML: Should contains " + TAG_RATING_SYSTEM_DEFINITION);
}
break;
case XmlPullParser.END_TAG:
if (TAG_RATING_SYSTEM_DEFINITIONS.equals(parser.getName())) {
eventType = parser.next();
assertEquals(
eventType,
XmlPullParser.END_DOCUMENT,
"Malformed XML: Should end with tag "
+ TAG_RATING_SYSTEM_DEFINITIONS);
return ratingSystems;
} else {
checkVersion(
"Malformed XML: Should end with tag "
+ TAG_RATING_SYSTEM_DEFINITIONS);
}
}
}
throw new XmlPullParserException(
TAG_RATING_SYSTEM_DEFINITIONS
+ " section is incomplete or section ending tag is missing");
}
private static void assertEquals(int a, int b, String msg) throws XmlPullParserException {
if (a != b) {
throw new XmlPullParserException(msg);
}
}
private static void assertEquals(String a, String b, String msg) throws XmlPullParserException {
if (!b.equals(a)) {
throw new XmlPullParserException(msg);
}
}
private void checkVersion(String msg) throws XmlPullParserException {
if (!VERSION_CODE.equals(mXmlVersionCode)) {
throw new XmlPullParserException(msg);
}
}
private ContentRatingSystem parseRatingSystemDefinition(
XmlResourceParser parser, String domain, boolean isCustom)
throws XmlPullParserException, IOException {
ContentRatingSystem.Builder builder = new ContentRatingSystem.Builder(mContext);
builder.setDomain(domain);
for (int i = 0; i < parser.getAttributeCount(); i++) {
String attr = parser.getAttributeName(i);
switch (attr) {
case ATTR_NAME:
builder.setName(parser.getAttributeValue(i));
break;
case ATTR_COUNTRY:
for (String country : parser.getAttributeValue(i).split("\\s*,\\s*")) {
builder.addCountry(country);
}
break;
case ATTR_TITLE:
builder.setTitle(getTitle(parser, i));
break;
case ATTR_DESCRIPTION:
builder.setDescription(
mResources.getString(parser.getAttributeResourceValue(i, 0)));
break;
default:
checkVersion(
"Malformed XML: Unknown attribute "
+ attr
+ " in "
+ TAG_RATING_SYSTEM_DEFINITION);
}
}
while (parser.next() != XmlPullParser.END_DOCUMENT) {
int eventType = parser.getEventType();
switch (eventType) {
case XmlPullParser.START_TAG:
String tag = parser.getName();
switch (tag) {
case TAG_RATING_DEFINITION:
builder.addRatingBuilder(parseRatingDefinition(parser));
break;
case TAG_SUB_RATING_DEFINITION:
builder.addSubRatingBuilder(parseSubRatingDefinition(parser));
break;
case TAG_RATING_ORDER:
builder.addOrderBuilder(parseOrder(parser));
break;
default:
checkVersion(
"Malformed XML: Unknown tag "
+ tag
+ " in "
+ TAG_RATING_SYSTEM_DEFINITION);
}
break;
case XmlPullParser.END_TAG:
if (TAG_RATING_SYSTEM_DEFINITION.equals(parser.getName())) {
builder.setIsCustom(isCustom);
return builder.build();
} else {
checkVersion(
"Malformed XML: Tag mismatch for " + TAG_RATING_SYSTEM_DEFINITION);
}
break;
default:
checkVersion(
"Malformed XML: Unknown event type "
+ eventType
+ " in "
+ TAG_RATING_SYSTEM_DEFINITION);
}
}
throw new XmlPullParserException(
TAG_RATING_SYSTEM_DEFINITION
+ " section is incomplete or section ending tag is missing");
}
private Rating.Builder parseRatingDefinition(XmlResourceParser parser)
throws XmlPullParserException, IOException {
Rating.Builder builder = new Rating.Builder();
for (int i = 0; i < parser.getAttributeCount(); i++) {
String attr = parser.getAttributeName(i);
switch (attr) {
case ATTR_NAME:
builder.setName(parser.getAttributeValue(i));
break;
case ATTR_TITLE:
builder.setTitle(getTitle(parser, i));
break;
case ATTR_DESCRIPTION:
builder.setDescription(
mResources.getString(parser.getAttributeResourceValue(i, 0)));
break;
case ATTR_ICON:
builder.setIcon(
mResources.getDrawable(parser.getAttributeResourceValue(i, 0), null));
break;
case ATTR_CONTENT_AGE_HINT:
int contentAgeHint = -1;
try {
contentAgeHint = Integer.parseInt(parser.getAttributeValue(i));
} catch (NumberFormatException ignored) {
}
if (contentAgeHint < 0) {
throw new XmlPullParserException(
"Malformed XML: "
+ ATTR_CONTENT_AGE_HINT
+ " should be a non-negative number");
}
builder.setContentAgeHint(contentAgeHint);
break;
default:
checkVersion(
"Malformed XML: Unknown attribute "
+ attr
+ " in "
+ TAG_RATING_DEFINITION);
}
}
while (parser.next() != XmlPullParser.END_DOCUMENT) {
switch (parser.getEventType()) {
case XmlPullParser.START_TAG:
if (TAG_SUB_RATING.equals(parser.getName())) {
builder = parseSubRating(parser, builder);
} else {
checkVersion(
("Malformed XML: Only "
+ TAG_SUB_RATING
+ " is allowed in "
+ TAG_RATING_DEFINITION));
}
break;
case XmlPullParser.END_TAG:
if (TAG_RATING_DEFINITION.equals(parser.getName())) {
return builder;
} else {
checkVersion("Malformed XML: Tag mismatch for " + TAG_RATING_DEFINITION);
}
}
}
throw new XmlPullParserException(
TAG_RATING_DEFINITION + " section is incomplete or section ending tag is missing");
}
private SubRating.Builder parseSubRatingDefinition(XmlResourceParser parser)
throws XmlPullParserException, IOException {
SubRating.Builder builder = new SubRating.Builder();
for (int i = 0; i < parser.getAttributeCount(); i++) {
String attr = parser.getAttributeName(i);
switch (attr) {
case ATTR_NAME:
builder.setName(parser.getAttributeValue(i));
break;
case ATTR_TITLE:
builder.setTitle(getTitle(parser, i));
break;
case ATTR_DESCRIPTION:
builder.setDescription(
mResources.getString(parser.getAttributeResourceValue(i, 0)));
break;
case ATTR_ICON:
builder.setIcon(
mResources.getDrawable(parser.getAttributeResourceValue(i, 0), null));
break;
default:
checkVersion(
"Malformed XML: Unknown attribute "
+ attr
+ " in "
+ TAG_SUB_RATING_DEFINITION);
}
}
while (parser.next() != XmlPullParser.END_DOCUMENT) {
switch (parser.getEventType()) {
case XmlPullParser.END_TAG:
if (TAG_SUB_RATING_DEFINITION.equals(parser.getName())) {
return builder;
} else {
checkVersion(
"Malformed XML: " + TAG_SUB_RATING_DEFINITION + " isn't closed");
}
break;
default:
checkVersion("Malformed XML: " + TAG_SUB_RATING_DEFINITION + " has child");
}
}
throw new XmlPullParserException(
TAG_SUB_RATING_DEFINITION
+ " section is incomplete or section ending tag is missing");
}
private Order.Builder parseOrder(XmlResourceParser parser)
throws XmlPullParserException, IOException {
Order.Builder builder = new Order.Builder();
assertEquals(
parser.getAttributeCount(),
0,
"Malformed XML: Attribute isn't allowed in " + TAG_RATING_ORDER);
while (parser.next() != XmlPullParser.END_DOCUMENT) {
switch (parser.getEventType()) {
case XmlPullParser.START_TAG:
if (TAG_RATING.equals(parser.getName())) {
builder = parseRating(parser, builder);
} else {
checkVersion(
"Malformed XML: Only "
+ TAG_RATING
+ " is allowed in "
+ TAG_RATING_ORDER);
}
break;
case XmlPullParser.END_TAG:
assertEquals(
parser.getName(),
TAG_RATING_ORDER,
"Malformed XML: Tag mismatch for " + TAG_RATING_ORDER);
return builder;
}
}
throw new XmlPullParserException(
TAG_RATING_ORDER + " section is incomplete or section ending tag is missing");
}
private Order.Builder parseRating(XmlResourceParser parser, Order.Builder builder)
throws XmlPullParserException, IOException {
for (int i = 0; i < parser.getAttributeCount(); i++) {
String attr = parser.getAttributeName(i);
switch (attr) {
case ATTR_NAME:
builder.addRatingName(parser.getAttributeValue(i));
break;
default:
checkVersion(
"Malformed XML: "
+ TAG_RATING_ORDER
+ " should only contain "
+ ATTR_NAME);
}
}
while (parser.next() != XmlPullParser.END_DOCUMENT) {
if (parser.getEventType() == XmlPullParser.END_TAG) {
if (TAG_RATING.equals(parser.getName())) {
return builder;
} else {
checkVersion("Malformed XML: " + TAG_RATING + " has child");
}
}
}
throw new XmlPullParserException(
TAG_RATING + " section is incomplete or section ending tag is missing");
}
private Rating.Builder parseSubRating(XmlResourceParser parser, Rating.Builder builder)
throws XmlPullParserException, IOException {
for (int i = 0; i < parser.getAttributeCount(); i++) {
String attr = parser.getAttributeName(i);
switch (attr) {
case ATTR_NAME:
builder.addSubRatingName(parser.getAttributeValue(i));
break;
default:
checkVersion(
"Malformed XML: "
+ TAG_SUB_RATING
+ " should only contain "
+ ATTR_NAME);
}
}
while (parser.next() != XmlPullParser.END_DOCUMENT) {
if (parser.getEventType() == XmlPullParser.END_TAG) {
if (TAG_SUB_RATING.equals(parser.getName())) {
return builder;
} else {
checkVersion("Malformed XML: " + TAG_SUB_RATING + " has child");
}
}
}
throw new XmlPullParserException(
TAG_SUB_RATING + " section is incomplete or section ending tag is missing");
}
// Title might be a resource id or a string value. Try loading as an id first, then use the
// string if that fails.
private String getTitle(XmlResourceParser parser, int index) {
int titleResId = parser.getAttributeResourceValue(index, 0);
if (titleResId != 0) {
return mResources.getString(titleResId);
}
return parser.getAttributeValue(index);
}
}