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
17package com.android.tv.parental;
18
19import android.content.Context;
20import android.graphics.drawable.Drawable;
21import android.media.tv.TvContentRating;
22import android.text.TextUtils;
23
24import com.android.tv.R;
25
26import java.util.ArrayList;
27import java.util.Comparator;
28import java.util.List;
29import java.util.Locale;
30
31public 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