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 24 import com.android.tv.R; 25 26 import java.util.ArrayList; 27 import java.util.Comparator; 28 import java.util.List; 29 import java.util.Locale; 30 31 public class ContentRatingSystem { 32 /* 33 * A comparator that implements the display order of a group of content rating systems. 34 */ 35 public static final Comparator<ContentRatingSystem> DISPLAY_NAME_COMPARATOR = 36 new Comparator<ContentRatingSystem>() { 37 @Override 38 public int compare(ContentRatingSystem s1, ContentRatingSystem s2) { 39 String name1 = s1.getDisplayName(); 40 String name2 = s2.getDisplayName(); 41 return name1.compareTo(name2); 42 } 43 }; 44 45 private static final String DELIMITER = "/"; 46 47 // Name of this content rating system. It should be unique in an XML file. 48 private final String mName; 49 50 // Domain of this content rating system. It's package name now. 51 private final String mDomain; 52 53 // Title of this content rating system. (e.g. TV-PG) 54 private final String mTitle; 55 56 // Description of this content rating system. 57 private final String mDescription; 58 59 // Country code of this content rating system. 60 private final List<String> mCountries; 61 62 // Display name of this content rating system consisting of the associated country 63 // and its title. For example, "Canada (French)" 64 private final String mDisplayName; 65 66 // Ordered list of main content ratings. UX should respect the order. 67 private final List<Rating> mRatings; 68 69 // Ordered list of sub content ratings. UX should respect the order. 70 private final List<SubRating> mSubRatings; 71 72 // List of orders. This describes the automatic lock/unlock relationship between ratings. 73 // For example, let say we have following order. 74 // <order> 75 // <rating android:name="US_TVPG_Y" /> 76 // <rating android:name="US_TVPG_Y7" /> 77 // </order> 78 // This means that locking US_TVPG_Y7 automatically locks US_TVPG_Y and 79 // unlocking US_TVPG_Y automatically unlocks US_TVPG_Y7 from the UX. 80 // An user can still unlock US_TVPG_Y while US_TVPG_Y7 is locked by manually. 81 private final List<Order> mOrders; 82 83 private final boolean mIsCustom; 84 85 public String getId() { 86 return mDomain + DELIMITER + mName; 87 } 88 89 public String getName(){ 90 return mName; 91 } 92 93 public String getDomain() { 94 return mDomain; 95 } 96 97 public String getTitle(){ 98 return mTitle; 99 } 100 101 public String getDescription(){ 102 return mDescription; 103 } 104 105 public List<String> getCountries(){ 106 return mCountries; 107 } 108 109 public List<Rating> getRatings(){ 110 return mRatings; 111 } 112 113 public Rating getRating(String name) { 114 for (Rating rating : mRatings) { 115 if (TextUtils.equals(rating.getName(), name)) { 116 return rating; 117 } 118 } 119 return null; 120 } 121 122 public List<SubRating> getSubRatings(){ 123 return mSubRatings; 124 } 125 126 public List<Order> getOrders(){ 127 return mOrders; 128 } 129 130 /** 131 * Returns the display name of the content rating system consisting of the associated country 132 * and its title. For example, "Canada (French)". 133 */ 134 public String getDisplayName() { 135 return mDisplayName; 136 } 137 138 public boolean isCustom() { 139 return mIsCustom; 140 } 141 142 /** 143 * Returns true if the ratings is owned by this content rating system. 144 */ 145 public boolean ownsRating(TvContentRating rating) { 146 return mDomain.equals(rating.getDomain()) && mName.equals(rating.getRatingSystem()); 147 } 148 149 @Override 150 public boolean equals(Object obj) { 151 if (obj instanceof ContentRatingSystem) { 152 ContentRatingSystem other = (ContentRatingSystem) obj; 153 return this.mName.equals(other.mName) && this.mDomain.equals(other.mDomain); 154 } 155 return false; 156 } 157 158 @Override 159 public int hashCode() { 160 return 31 * mName.hashCode() + mDomain.hashCode(); 161 } 162 163 private ContentRatingSystem( 164 String name, String domain, String title, String description, List<String> countries, 165 String displayName, List<Rating> ratings, List<SubRating> subRatings, 166 List<Order> orders, 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 191 public Builder(Context context) { 192 mContext = context; 193 } 194 195 public void setName(String name) { 196 mName = name; 197 } 198 199 public void setDomain(String domain) { 200 mDomain = domain; 201 } 202 203 public void setTitle(String title) { 204 mTitle = title; 205 } 206 207 public void setDescription(String description) { 208 mDescription = description; 209 } 210 211 public void addCountry(String country) { 212 if (mCountries == null) { 213 mCountries = new ArrayList<>(); 214 } 215 mCountries.add(new Locale("", country).getCountry()); 216 } 217 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 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 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 241 public void setIsCustom(boolean isCustom) { 242 mIsCustom = isCustom; 243 } 244 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("Subrating " + subRating.getName() + 302 " 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(mName, mDomain, mTitle, mDescription, mCountries, 314 displayName, ratings, subRatings, orders, mIsCustom); 315 } 316 } 317 318 public static class Rating { 319 private final String mName; 320 private final String mTitle; 321 private final String mDescription; 322 private final Drawable mIcon; 323 private final int mContentAgeHint; 324 private final List<SubRating> mSubRatings; 325 326 public String getName() { 327 return mName; 328 } 329 330 public String getTitle() { 331 return mTitle; 332 } 333 334 public String getDescription() { 335 return mDescription; 336 } 337 338 public Drawable getIcon() { 339 return mIcon; 340 } 341 342 public int getAgeHint() { 343 return mContentAgeHint; 344 } 345 346 public List<SubRating> getSubRatings() { 347 return mSubRatings; 348 } 349 350 private Rating(String name, String title, String description, Drawable icon, 351 int contentAgeHint, List<SubRating> subRatings) { 352 mName = name; 353 mTitle = title; 354 mDescription = description; 355 mIcon = icon; 356 mContentAgeHint = contentAgeHint; 357 mSubRatings = subRatings; 358 } 359 360 public static class Builder { 361 private String mName; 362 private String mTitle; 363 private String mDescription; 364 private Drawable mIcon; 365 private int mContentAgeHint = -1; 366 private final List<String> mSubRatingNames = new ArrayList<>(); 367 368 public Builder() { 369 } 370 371 public void setName(String name) { 372 mName = name; 373 } 374 375 public void setTitle(String title) { 376 mTitle = title; 377 } 378 379 public void setDescription(String description) { 380 mDescription = description; 381 } 382 383 public void setIcon(Drawable icon) { 384 mIcon = icon; 385 } 386 387 public void setContentAgeHint(int contentAgeHint) { 388 mContentAgeHint = contentAgeHint; 389 } 390 391 public void addSubRatingName(String subRatingName) { 392 mSubRatingNames.add(subRatingName); 393 } 394 395 private Rating build(List<SubRating> allDefinedSubRatings) { 396 if (TextUtils.isEmpty(mName)) { 397 throw new IllegalArgumentException("A rating should have non-empty name"); 398 } 399 if (allDefinedSubRatings == null && mSubRatingNames.size() > 0) { 400 throw new IllegalArgumentException("Invalid subrating for rating " + mName); 401 } 402 if (mContentAgeHint < 0) { 403 throw new IllegalArgumentException("Rating " + mName + " should define " + 404 "non-negative contentAgeHint"); 405 } 406 407 List<SubRating> subRatings = new ArrayList<>(); 408 for (String subRatingId : mSubRatingNames) { 409 boolean found = false; 410 for (SubRating subRating : allDefinedSubRatings) { 411 if (subRatingId.equals(subRating.getName())) { 412 found = true; 413 subRatings.add(subRating); 414 break; 415 } 416 } 417 if (!found) { 418 throw new IllegalArgumentException("Unknown subrating name " + subRatingId + 419 " in rating " + mName); 420 } 421 } 422 return new Rating( 423 mName, mTitle, mDescription, mIcon, mContentAgeHint, subRatings); 424 } 425 } 426 } 427 428 public static class SubRating { 429 private final String mName; 430 private final String mTitle; 431 private final String mDescription; 432 private final Drawable mIcon; 433 434 public String getName() { 435 return mName; 436 } 437 438 public String getTitle() { 439 return mTitle; 440 } 441 442 public String getDescription() { 443 return mDescription; 444 } 445 446 public Drawable getIcon() { 447 return mIcon; 448 } 449 450 private SubRating(String name, String title, String description, Drawable icon) { 451 mName = name; 452 mTitle = title; 453 mDescription = description; 454 mIcon = icon; 455 } 456 457 public static class Builder { 458 private String mName; 459 private String mTitle; 460 private String mDescription; 461 private Drawable mIcon; 462 463 public Builder() { 464 } 465 466 public void setName(String name) { 467 mName = name; 468 } 469 470 public void setTitle(String title) { 471 mTitle = title; 472 } 473 474 public void setDescription(String description) { 475 mDescription = description; 476 } 477 478 public void setIcon(Drawable icon) { 479 mIcon = icon; 480 } 481 482 private SubRating build() { 483 if (TextUtils.isEmpty(mName)) { 484 throw new IllegalArgumentException("A subrating should have non-empty name"); 485 } 486 return new SubRating(mName, mTitle, mDescription, mIcon); 487 } 488 } 489 } 490 491 public static class Order { 492 private final List<Rating> mRatingOrder; 493 494 public List<Rating> getRatingOrder() { 495 return mRatingOrder; 496 } 497 498 private Order(List<Rating> ratingOrder) { 499 mRatingOrder = ratingOrder; 500 } 501 502 /** 503 * Returns index of the rating in this order. 504 * Returns -1 if this order doesn't contain the rating. 505 */ 506 public int getRatingIndex(Rating rating) { 507 for (int i = 0; i < mRatingOrder.size(); i++) { 508 if (mRatingOrder.get(i).getName().equals(rating.getName())) { 509 return i; 510 } 511 } 512 return -1; 513 } 514 515 public static class Builder { 516 private final List<String> mRatingNames = new ArrayList<>(); 517 518 public Builder() { 519 } 520 521 private Order build(List<Rating> ratings) { 522 List<Rating> ratingOrder = new ArrayList<>(); 523 for (String ratingName : mRatingNames) { 524 boolean found = false; 525 for (Rating rating : ratings) { 526 if (ratingName.equals(rating.getName())) { 527 found = true; 528 ratingOrder.add(rating); 529 break; 530 } 531 } 532 533 if (!found) { 534 throw new IllegalArgumentException("Unknown rating " + ratingName + 535 " in rating-order tag"); 536 } 537 } 538 539 return new Order(ratingOrder); 540 } 541 542 public void addRatingName(String name) { 543 mRatingNames.add(name); 544 } 545 } 546 } 547 } 548