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