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