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 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