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