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.Context; 20 import android.graphics.drawable.Drawable; 21 import android.media.tv.TvContentRating; 22 import android.text.TextUtils; 23 import com.android.tv.R; 24 import java.util.ArrayList; 25 import java.util.Comparator; 26 import java.util.List; 27 import java.util.Locale; 28 29 public class ContentRatingSystem { 30 /* 31 * A comparator that implements the display order of a group of content rating systems. 32 */ 33 public static final Comparator<ContentRatingSystem> DISPLAY_NAME_COMPARATOR = 34 (ContentRatingSystem s1, ContentRatingSystem s2) -> { 35 String name1 = s1.getDisplayName(); 36 String name2 = s2.getDisplayName(); 37 return name1.compareTo(name2); 38 }; 39 40 private static final String DELIMITER = "/"; 41 42 // Name of this content rating system. It should be unique in an XML file. 43 private final String mName; 44 45 // Domain of this content rating system. It's package name now. 46 private final String mDomain; 47 48 // Title of this content rating system. (e.g. TV-PG) 49 private final String mTitle; 50 51 // Description of this content rating system. 52 private final String mDescription; 53 54 // Country code of this content rating system. 55 private final List<String> mCountries; 56 57 // Display name of this content rating system consisting of the associated country 58 // and its title. For example, "Canada (French)" 59 private final String mDisplayName; 60 61 // Ordered list of main content ratings. UX should respect the order. 62 private final List<Rating> mRatings; 63 64 // Ordered list of sub content ratings. UX should respect the order. 65 private final List<SubRating> mSubRatings; 66 67 // List of orders. This describes the automatic lock/unlock relationship between ratings. 68 // For example, let say we have following order. 69 // <order> 70 // <rating android:name="US_TVPG_Y" /> 71 // <rating android:name="US_TVPG_Y7" /> 72 // </order> 73 // This means that locking US_TVPG_Y7 automatically locks US_TVPG_Y and 74 // unlocking US_TVPG_Y automatically unlocks US_TVPG_Y7 from the UX. 75 // An user can still unlock US_TVPG_Y while US_TVPG_Y7 is locked by manually. 76 private final List<Order> mOrders; 77 78 private final boolean mIsCustom; 79 getId()80 public String getId() { 81 return mDomain + DELIMITER + mName; 82 } 83 getName()84 public String getName() { 85 return mName; 86 } 87 getDomain()88 public String getDomain() { 89 return mDomain; 90 } 91 getTitle()92 public String getTitle() { 93 return mTitle; 94 } 95 getDescription()96 public String getDescription() { 97 return mDescription; 98 } 99 getCountries()100 public List<String> getCountries() { 101 return mCountries; 102 } 103 getRatings()104 public List<Rating> getRatings() { 105 return mRatings; 106 } 107 getRating(String name)108 public Rating getRating(String name) { 109 for (Rating rating : mRatings) { 110 if (TextUtils.equals(rating.getName(), name)) { 111 return rating; 112 } 113 } 114 return null; 115 } 116 getSubRatings()117 public List<SubRating> getSubRatings() { 118 return mSubRatings; 119 } 120 getOrders()121 public List<Order> getOrders() { 122 return mOrders; 123 } 124 125 /** 126 * Returns the display name of the content rating system consisting of the associated country 127 * and its title. For example, "Canada (French)". 128 */ getDisplayName()129 public String getDisplayName() { 130 return mDisplayName; 131 } 132 isCustom()133 public boolean isCustom() { 134 return mIsCustom; 135 } 136 137 /** Returns true if the ratings is owned by this content rating system. */ ownsRating(TvContentRating rating)138 public boolean ownsRating(TvContentRating rating) { 139 return mDomain.equals(rating.getDomain()) && mName.equals(rating.getRatingSystem()); 140 } 141 142 @Override equals(Object obj)143 public boolean equals(Object obj) { 144 if (obj instanceof ContentRatingSystem) { 145 ContentRatingSystem other = (ContentRatingSystem) obj; 146 return this.mName.equals(other.mName) && this.mDomain.equals(other.mDomain); 147 } 148 return false; 149 } 150 151 @Override hashCode()152 public int hashCode() { 153 return 31 * mName.hashCode() + mDomain.hashCode(); 154 } 155 ContentRatingSystem( String name, String domain, String title, String description, List<String> countries, String displayName, List<Rating> ratings, List<SubRating> subRatings, List<Order> orders, boolean isCustom)156 private ContentRatingSystem( 157 String name, 158 String domain, 159 String title, 160 String description, 161 List<String> countries, 162 String displayName, 163 List<Rating> ratings, 164 List<SubRating> subRatings, 165 List<Order> orders, 166 boolean isCustom) { 167 mName = name; 168 mDomain = domain; 169 mTitle = title; 170 mDescription = description; 171 mCountries = countries; 172 mDisplayName = displayName; 173 mRatings = ratings; 174 mSubRatings = subRatings; 175 mOrders = orders; 176 mIsCustom = isCustom; 177 } 178 179 public static class Builder { 180 private final Context mContext; 181 private String mName; 182 private String mDomain; 183 private String mTitle; 184 private String mDescription; 185 private List<String> mCountries; 186 private final List<Rating.Builder> mRatingBuilders = new ArrayList<>(); 187 private final List<SubRating.Builder> mSubRatingBuilders = new ArrayList<>(); 188 private final List<Order.Builder> mOrderBuilders = new ArrayList<>(); 189 private boolean mIsCustom; 190 Builder(Context context)191 public Builder(Context context) { 192 mContext = context; 193 } 194 setName(String name)195 public void setName(String name) { 196 mName = name; 197 } 198 setDomain(String domain)199 public void setDomain(String domain) { 200 mDomain = domain; 201 } 202 setTitle(String title)203 public void setTitle(String title) { 204 mTitle = title; 205 } 206 setDescription(String description)207 public void setDescription(String description) { 208 mDescription = description; 209 } 210 addCountry(String country)211 public void addCountry(String country) { 212 if (mCountries == null) { 213 mCountries = new ArrayList<>(); 214 } 215 mCountries.add(new Locale("", country).getCountry()); 216 } 217 addRatingBuilder(Rating.Builder ratingBuilder)218 public void addRatingBuilder(Rating.Builder ratingBuilder) { 219 // To provide easy access to the SubRatings in it, 220 // Rating has reference to SubRating, not Name of it. 221 // (Note that Rating/SubRating is ordered list so we cannot use Map) 222 // To do so, we need to have list of all SubRatings which might not be available 223 // at this moment. Keep builders here and build it with SubRatings later. 224 mRatingBuilders.add(ratingBuilder); 225 } 226 addSubRatingBuilder(SubRating.Builder subRatingBuilder)227 public void addSubRatingBuilder(SubRating.Builder subRatingBuilder) { 228 // SubRatings would be built rather to keep consistency with other fields. 229 mSubRatingBuilders.add(subRatingBuilder); 230 } 231 addOrderBuilder(Order.Builder orderBuilder)232 public void addOrderBuilder(Order.Builder orderBuilder) { 233 // To provide easy access to the Ratings in it, 234 // Order has reference to Rating, not Name of it. 235 // (Note that Rating/SubRating is ordered list so we cannot use Map) 236 // To do so, we need to have list of all Rating which might not be available 237 // at this moment. Keep builders here and build it with Ratings later. 238 mOrderBuilders.add(orderBuilder); 239 } 240 setIsCustom(boolean isCustom)241 public void setIsCustom(boolean isCustom) { 242 mIsCustom = isCustom; 243 } 244 build()245 public ContentRatingSystem build() { 246 if (TextUtils.isEmpty(mName)) { 247 throw new IllegalArgumentException("Name cannot be empty"); 248 } 249 if (TextUtils.isEmpty(mDomain)) { 250 throw new IllegalArgumentException("Domain cannot be empty"); 251 } 252 253 StringBuilder sb = new StringBuilder(); 254 if (mCountries != null) { 255 if (mCountries.size() == 1) { 256 sb.append(new Locale("", mCountries.get(0)).getDisplayCountry()); 257 } else if (mCountries.size() > 1) { 258 Locale locale = Locale.getDefault(); 259 if (mCountries.contains(locale.getCountry())) { 260 // Shows the country name instead of "Other countries" if the current 261 // country is one of the countries this rating system applies to. 262 sb.append(locale.getDisplayCountry()); 263 } else { 264 sb.append(mContext.getString(R.string.other_countries)); 265 } 266 } 267 } 268 if (!TextUtils.isEmpty(mTitle)) { 269 sb.append(" ("); 270 sb.append(mTitle); 271 sb.append(")"); 272 } 273 String displayName = sb.toString(); 274 275 List<SubRating> subRatings = new ArrayList<>(); 276 if (mSubRatingBuilders != null) { 277 for (SubRating.Builder builder : mSubRatingBuilders) { 278 subRatings.add(builder.build()); 279 } 280 } 281 282 if (mRatingBuilders.size() <= 0) { 283 throw new IllegalArgumentException("Rating isn't available."); 284 } 285 List<Rating> ratings = new ArrayList<>(); 286 // Map string ID to object. 287 for (Rating.Builder builder : mRatingBuilders) { 288 ratings.add(builder.build(subRatings)); 289 } 290 291 // Sanity check. 292 for (SubRating subRating : subRatings) { 293 boolean used = false; 294 for (Rating rating : ratings) { 295 if (rating.getSubRatings().contains(subRating)) { 296 used = true; 297 break; 298 } 299 } 300 if (!used) { 301 throw new IllegalArgumentException( 302 "Subrating " + subRating.getName() + " isn't used by any rating"); 303 } 304 } 305 306 List<Order> orders = new ArrayList<>(); 307 if (mOrderBuilders != null) { 308 for (Order.Builder builder : mOrderBuilders) { 309 orders.add(builder.build(ratings)); 310 } 311 } 312 313 return new ContentRatingSystem( 314 mName, 315 mDomain, 316 mTitle, 317 mDescription, 318 mCountries, 319 displayName, 320 ratings, 321 subRatings, 322 orders, 323 mIsCustom); 324 } 325 } 326 327 public static class Rating { 328 private final String mName; 329 private final String mTitle; 330 private final String mDescription; 331 private final Drawable mIcon; 332 private final int mContentAgeHint; 333 private final List<SubRating> mSubRatings; 334 getName()335 public String getName() { 336 return mName; 337 } 338 getTitle()339 public String getTitle() { 340 return mTitle; 341 } 342 getDescription()343 public String getDescription() { 344 return mDescription; 345 } 346 getIcon()347 public Drawable getIcon() { 348 return mIcon; 349 } 350 getAgeHint()351 public int getAgeHint() { 352 return mContentAgeHint; 353 } 354 getSubRatings()355 public List<SubRating> getSubRatings() { 356 return mSubRatings; 357 } 358 Rating( String name, String title, String description, Drawable icon, int contentAgeHint, List<SubRating> subRatings)359 private Rating( 360 String name, 361 String title, 362 String description, 363 Drawable icon, 364 int contentAgeHint, 365 List<SubRating> subRatings) { 366 mName = name; 367 mTitle = title; 368 mDescription = description; 369 mIcon = icon; 370 mContentAgeHint = contentAgeHint; 371 mSubRatings = subRatings; 372 } 373 374 public static class Builder { 375 private String mName; 376 private String mTitle; 377 private String mDescription; 378 private Drawable mIcon; 379 private int mContentAgeHint = -1; 380 private final List<String> mSubRatingNames = new ArrayList<>(); 381 Builder()382 public Builder() {} 383 setName(String name)384 public void setName(String name) { 385 mName = name; 386 } 387 setTitle(String title)388 public void setTitle(String title) { 389 mTitle = title; 390 } 391 setDescription(String description)392 public void setDescription(String description) { 393 mDescription = description; 394 } 395 setIcon(Drawable icon)396 public void setIcon(Drawable icon) { 397 mIcon = icon; 398 } 399 setContentAgeHint(int contentAgeHint)400 public void setContentAgeHint(int contentAgeHint) { 401 mContentAgeHint = contentAgeHint; 402 } 403 addSubRatingName(String subRatingName)404 public void addSubRatingName(String subRatingName) { 405 mSubRatingNames.add(subRatingName); 406 } 407 build(List<SubRating> allDefinedSubRatings)408 private Rating build(List<SubRating> allDefinedSubRatings) { 409 if (TextUtils.isEmpty(mName)) { 410 throw new IllegalArgumentException("A rating should have non-empty name"); 411 } 412 if (allDefinedSubRatings == null && mSubRatingNames.size() > 0) { 413 throw new IllegalArgumentException("Invalid subrating for rating " + mName); 414 } 415 if (mContentAgeHint < 0) { 416 throw new IllegalArgumentException( 417 "Rating " + mName + " should define " + "non-negative contentAgeHint"); 418 } 419 420 List<SubRating> subRatings = new ArrayList<>(); 421 for (String subRatingId : mSubRatingNames) { 422 boolean found = false; 423 for (SubRating subRating : allDefinedSubRatings) { 424 if (subRatingId.equals(subRating.getName())) { 425 found = true; 426 subRatings.add(subRating); 427 break; 428 } 429 } 430 if (!found) { 431 throw new IllegalArgumentException( 432 "Unknown subrating name " + subRatingId + " in rating " + mName); 433 } 434 } 435 return new Rating(mName, mTitle, mDescription, mIcon, mContentAgeHint, subRatings); 436 } 437 } 438 } 439 440 public static class SubRating { 441 private final String mName; 442 private final String mTitle; 443 private final String mDescription; 444 private final Drawable mIcon; 445 getName()446 public String getName() { 447 return mName; 448 } 449 getTitle()450 public String getTitle() { 451 return mTitle; 452 } 453 getDescription()454 public String getDescription() { 455 return mDescription; 456 } 457 getIcon()458 public Drawable getIcon() { 459 return mIcon; 460 } 461 SubRating(String name, String title, String description, Drawable icon)462 private SubRating(String name, String title, String description, Drawable icon) { 463 mName = name; 464 mTitle = title; 465 mDescription = description; 466 mIcon = icon; 467 } 468 469 public static class Builder { 470 private String mName; 471 private String mTitle; 472 private String mDescription; 473 private Drawable mIcon; 474 Builder()475 public Builder() {} 476 setName(String name)477 public void setName(String name) { 478 mName = name; 479 } 480 setTitle(String title)481 public void setTitle(String title) { 482 mTitle = title; 483 } 484 setDescription(String description)485 public void setDescription(String description) { 486 mDescription = description; 487 } 488 setIcon(Drawable icon)489 public void setIcon(Drawable icon) { 490 mIcon = icon; 491 } 492 build()493 private SubRating build() { 494 if (TextUtils.isEmpty(mName)) { 495 throw new IllegalArgumentException("A subrating should have non-empty name"); 496 } 497 return new SubRating(mName, mTitle, mDescription, mIcon); 498 } 499 } 500 } 501 502 public static class Order { 503 private final List<Rating> mRatingOrder; 504 getRatingOrder()505 public List<Rating> getRatingOrder() { 506 return mRatingOrder; 507 } 508 Order(List<Rating> ratingOrder)509 private Order(List<Rating> ratingOrder) { 510 mRatingOrder = ratingOrder; 511 } 512 513 /** 514 * Returns index of the rating in this order. Returns -1 if this order doesn't contain the 515 * rating. 516 */ getRatingIndex(Rating rating)517 public int getRatingIndex(Rating rating) { 518 for (int i = 0; i < mRatingOrder.size(); i++) { 519 if (mRatingOrder.get(i).getName().equals(rating.getName())) { 520 return i; 521 } 522 } 523 return -1; 524 } 525 526 public static class Builder { 527 private final List<String> mRatingNames = new ArrayList<>(); 528 Builder()529 public Builder() {} 530 build(List<Rating> ratings)531 private Order build(List<Rating> ratings) { 532 List<Rating> ratingOrder = new ArrayList<>(); 533 for (String ratingName : mRatingNames) { 534 boolean found = false; 535 for (Rating rating : ratings) { 536 if (ratingName.equals(rating.getName())) { 537 found = true; 538 ratingOrder.add(rating); 539 break; 540 } 541 } 542 543 if (!found) { 544 throw new IllegalArgumentException( 545 "Unknown rating " + ratingName + " in rating-order tag"); 546 } 547 } 548 549 return new Order(ratingOrder); 550 } 551 addRatingName(String name)552 public void addRatingName(String name) { 553 mRatingNames.add(name); 554 } 555 } 556 } 557 } 558