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 
getId()85     public String getId() {
86         return mDomain + DELIMITER + mName;
87     }
88 
getName()89     public String getName(){
90         return mName;
91     }
92 
getDomain()93     public String getDomain() {
94         return mDomain;
95     }
96 
getTitle()97     public String getTitle(){
98         return mTitle;
99     }
100 
getDescription()101     public String getDescription(){
102         return mDescription;
103     }
104 
getCountries()105     public List<String> getCountries(){
106         return mCountries;
107     }
108 
getRatings()109     public List<Rating> getRatings(){
110         return mRatings;
111     }
112 
getSubRatings()113     public List<SubRating> getSubRatings(){
114         return mSubRatings;
115     }
116 
getOrders()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      */
getDisplayName()125     public String getDisplayName() {
126         return mDisplayName;
127     }
128 
isCustom()129     public boolean isCustom() {
130         return mIsCustom;
131     }
132 
133     /**
134      * Returns true if the ratings is owned by this content rating system.
135      */
ownsRating(TvContentRating rating)136     public boolean ownsRating(TvContentRating rating) {
137         return mDomain.equals(rating.getDomain()) && mName.equals(rating.getRatingSystem());
138     }
139 
140     @Override
equals(Object obj)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
hashCode()150     public int hashCode() {
151         return 31 * mName.hashCode() + mDomain.hashCode();
152     }
153 
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)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 
Builder(Context context)182         public Builder(Context context) {
183             mContext = context;
184         }
185 
setName(String name)186         public void setName(String name) {
187             mName = name;
188         }
189 
setDomain(String domain)190         public void setDomain(String domain) {
191             mDomain = domain;
192         }
193 
setTitle(String title)194         public void setTitle(String title) {
195             mTitle = title;
196         }
197 
setDescription(String description)198         public void setDescription(String description) {
199             mDescription = description;
200         }
201 
addCountry(String country)202         public void addCountry(String country) {
203             if (mCountries == null) {
204                 mCountries = new ArrayList<>();
205             }
206             mCountries.add(new Locale("", country).getCountry());
207         }
208 
addRatingBuilder(Rating.Builder ratingBuilder)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 
addSubRatingBuilder(SubRating.Builder subRatingBuilder)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 
addOrderBuilder(Order.Builder orderBuilder)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 
setIsCustom(boolean isCustom)232         public void setIsCustom(boolean isCustom) {
233             mIsCustom = isCustom;
234         }
235 
build()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 
getName()317         public String getName() {
318             return mName;
319         }
320 
getTitle()321         public String getTitle() {
322             return mTitle;
323         }
324 
getDescription()325         public String getDescription() {
326             return mDescription;
327         }
328 
getIcon()329         public Drawable getIcon() {
330             return mIcon;
331         }
332 
getAgeHint()333         public int getAgeHint() {
334             return mContentAgeHint;
335         }
336 
getSubRatings()337         public List<SubRating> getSubRatings() {
338             return mSubRatings;
339         }
340 
Rating(String name, String title, String description, Drawable icon, int contentAgeHint, List<SubRating> subRatings)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 
Builder()359             public Builder() {
360             }
361 
setName(String name)362             public void setName(String name) {
363                 mName = name;
364             }
365 
setTitle(String title)366             public void setTitle(String title) {
367                 mTitle = title;
368             }
369 
setDescription(String description)370             public void setDescription(String description) {
371                 mDescription = description;
372             }
373 
setIcon(Drawable icon)374             public void setIcon(Drawable icon) {
375                 mIcon = icon;
376             }
377 
setContentAgeHint(int contentAgeHint)378             public void setContentAgeHint(int contentAgeHint) {
379                 mContentAgeHint = contentAgeHint;
380             }
381 
addSubRatingName(String subRatingName)382             public void addSubRatingName(String subRatingName) {
383                 mSubRatingNames.add(subRatingName);
384             }
385 
build(List<SubRating> allDefinedSubRatings)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 
getName()425         public String getName() {
426             return mName;
427         }
428 
getTitle()429         public String getTitle() {
430             return mTitle;
431         }
432 
getDescription()433         public String getDescription() {
434             return mDescription;
435         }
436 
getIcon()437         public Drawable getIcon() {
438             return mIcon;
439         }
440 
SubRating(String name, String title, String description, Drawable icon)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 
Builder()454             public Builder() {
455             }
456 
setName(String name)457             public void setName(String name) {
458                 mName = name;
459             }
460 
setTitle(String title)461             public void setTitle(String title) {
462                 mTitle = title;
463             }
464 
setDescription(String description)465             public void setDescription(String description) {
466                 mDescription = description;
467             }
468 
setIcon(Drawable icon)469             public void setIcon(Drawable icon) {
470                 mIcon = icon;
471             }
472 
build()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 
getRatingOrder()485         public List<Rating> getRatingOrder() {
486             return mRatingOrder;
487         }
488 
Order(List<Rating> ratingOrder)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          */
getRatingIndex(Rating rating)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 
Builder()509             public Builder() {
510             }
511 
build(List<Rating> ratings)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 
addRatingName(String name)533             public void addRatingName(String name) {
534                 mRatingNames.add(name);
535             }
536         }
537     }
538 }
539