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.ui.sidepanel.parentalcontrols;
18 
19 import android.app.Activity;
20 import android.graphics.drawable.Drawable;
21 import android.media.tv.TvContentRating;
22 import android.os.Bundle;
23 import android.util.ArrayMap;
24 import android.util.SparseIntArray;
25 import android.view.View;
26 import android.widget.CompoundButton;
27 import android.widget.ImageView;
28 
29 import com.android.tv.MainActivity;
30 import com.android.tv.R;
31 import com.android.tv.dialog.WebDialogFragment;
32 import com.android.tv.license.LicenseUtils;
33 import com.android.tv.parental.ContentRatingSystem;
34 import com.android.tv.parental.ContentRatingSystem.Rating;
35 import com.android.tv.parental.ParentalControlSettings;
36 import com.android.tv.ui.sidepanel.CheckBoxItem;
37 import com.android.tv.ui.sidepanel.DividerItem;
38 import com.android.tv.ui.sidepanel.Item;
39 import com.android.tv.ui.sidepanel.RadioButtonItem;
40 import com.android.tv.ui.sidepanel.SideFragment;
41 import com.android.tv.util.TvSettings;
42 import com.android.tv.util.TvSettings.ContentRatingLevel;
43 
44 import com.google.common.collect.ImmutableList;
45 
46 import dagger.android.AndroidInjection;
47 
48 import com.android.tv.common.flags.LegacyFlags;
49 
50 import java.util.ArrayList;
51 import java.util.Collections;
52 import java.util.List;
53 import java.util.Map;
54 
55 import javax.inject.Inject;
56 
57 public class RatingsFragment extends SideFragment {
58     private static final SparseIntArray sLevelResourceIdMap;
59     private static final SparseIntArray sDescriptionResourceIdMap;
60     private static final String TRACKER_LABEL = "Ratings";
61     private int mItemsSize;
62 
63     @Inject LegacyFlags mLegacyFlags;
64 
65     static {
66         sLevelResourceIdMap = new SparseIntArray(5);
sLevelResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_NONE, R.string.option_rating_none)67         sLevelResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_NONE, R.string.option_rating_none);
sLevelResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_HIGH, R.string.option_rating_high)68         sLevelResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_HIGH, R.string.option_rating_high);
sLevelResourceIdMap.put( TvSettings.CONTENT_RATING_LEVEL_MEDIUM, R.string.option_rating_medium)69         sLevelResourceIdMap.put(
70                 TvSettings.CONTENT_RATING_LEVEL_MEDIUM, R.string.option_rating_medium);
sLevelResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_LOW, R.string.option_rating_low)71         sLevelResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_LOW, R.string.option_rating_low);
sLevelResourceIdMap.put( TvSettings.CONTENT_RATING_LEVEL_CUSTOM, R.string.option_rating_custom)72         sLevelResourceIdMap.put(
73                 TvSettings.CONTENT_RATING_LEVEL_CUSTOM, R.string.option_rating_custom);
74 
75         sDescriptionResourceIdMap = new SparseIntArray(sLevelResourceIdMap.size());
sDescriptionResourceIdMap.put( TvSettings.CONTENT_RATING_LEVEL_HIGH, R.string.option_rating_high_description)76         sDescriptionResourceIdMap.put(
77                 TvSettings.CONTENT_RATING_LEVEL_HIGH, R.string.option_rating_high_description);
sDescriptionResourceIdMap.put( TvSettings.CONTENT_RATING_LEVEL_MEDIUM, R.string.option_rating_medium_description)78         sDescriptionResourceIdMap.put(
79                 TvSettings.CONTENT_RATING_LEVEL_MEDIUM, R.string.option_rating_medium_description);
sDescriptionResourceIdMap.put( TvSettings.CONTENT_RATING_LEVEL_LOW, R.string.option_rating_low_description)80         sDescriptionResourceIdMap.put(
81                 TvSettings.CONTENT_RATING_LEVEL_LOW, R.string.option_rating_low_description);
sDescriptionResourceIdMap.put( TvSettings.CONTENT_RATING_LEVEL_CUSTOM, R.string.option_rating_custom_description)82         sDescriptionResourceIdMap.put(
83                 TvSettings.CONTENT_RATING_LEVEL_CUSTOM, R.string.option_rating_custom_description);
84     }
85 
86     private final List<RatingLevelItem> mRatingLevelItems = new ArrayList<>();
87     // A map from the rating system ID string to RatingItem objects.
88     private final Map<String, List<RatingItem>> mContentRatingSystemItemMap = new ArrayMap<>();
89     private CheckBoxItem mBlockUnratedItem;
90     private ParentalControlSettings mParentalControlSettings;
91 
getDescription(MainActivity tvActivity)92     public static String getDescription(MainActivity tvActivity) {
93         @ContentRatingLevel
94         int currentLevel = tvActivity.getParentalControlSettings().getContentRatingLevel();
95         if (sLevelResourceIdMap.indexOfKey(currentLevel) >= 0) {
96             return tvActivity.getString(sLevelResourceIdMap.get(currentLevel));
97         }
98         return null;
99     }
100 
101     @Override
getTitle()102     protected String getTitle() {
103         return getString(R.string.option_ratings);
104     }
105 
106     @Override
getTrackerLabel()107     public String getTrackerLabel() {
108         return TRACKER_LABEL;
109     }
110 
111     @Override
getItemList()112     protected List<Item> getItemList() {
113         List<Item> items = new ArrayList<>();
114 
115         if (mBlockUnratedItem != null && mLegacyFlags.enableUnratedContentSettings()) {
116             items.add(mBlockUnratedItem);
117             items.add(new DividerItem());
118         }
119 
120         mRatingLevelItems.clear();
121         for (int i = 0; i < sLevelResourceIdMap.size(); ++i) {
122             mRatingLevelItems.add(new RatingLevelItem(sLevelResourceIdMap.keyAt(i)));
123         }
124         updateRatingLevels();
125         items.addAll(mRatingLevelItems);
126 
127         mContentRatingSystemItemMap.clear();
128 
129         List<ContentRatingSystem> contentRatingSystems =
130                 getMainActivity().getContentRatingsManager().getContentRatingSystems();
131         Collections.sort(contentRatingSystems, ContentRatingSystem.DISPLAY_NAME_COMPARATOR);
132 
133         for (ContentRatingSystem s : contentRatingSystems) {
134             if (mParentalControlSettings.isContentRatingSystemEnabled(s)) {
135                 List<RatingItem> ratingItems = new ArrayList<>();
136                 boolean hasSubRating = false;
137                 items.add(new DividerItem(s.getDisplayName()));
138                 for (Rating rating : s.getRatings()) {
139                     RatingItem item =
140                             rating.getSubRatings().isEmpty()
141                                     ? new RatingItem(s, rating)
142                                     : new RatingWithSubItem(s, rating);
143                     items.add(item);
144                     if (rating.getSubRatings().isEmpty()) {
145                         ratingItems.add(item);
146                     } else {
147                         hasSubRating = true;
148                     }
149                 }
150                 // Only include rating systems that don't contain any sub ratings in the map for
151                 // simplicity.
152                 if (!hasSubRating) {
153                     mContentRatingSystemItemMap.put(s.getId(), ratingItems);
154                 }
155             }
156         }
157         if (LicenseUtils.hasRatingAttribution(getMainActivity().getAssets())) {
158             // Display the attribution if our content rating system is selected.
159             items.add(new DividerItem());
160             items.add(new AttributionItem(getMainActivity()));
161         }
162         mItemsSize = items.size();
163         return items;
164     }
165 
166     @Override
onCreate(Bundle savedInstanceState)167     public void onCreate(Bundle savedInstanceState) {
168         super.onCreate(savedInstanceState);
169         mParentalControlSettings = getMainActivity().getParentalControlSettings();
170         mParentalControlSettings.loadRatings();
171     }
172 
173     @Override
onAttach(Activity activity)174     public void onAttach(Activity activity) {
175         AndroidInjection.inject(this);
176         super.onAttach(activity);
177         if (mLegacyFlags.enableUnratedContentSettings()) {
178             mBlockUnratedItem =
179                     new CheckBoxItem(
180                             getResources().getString(R.string.option_block_unrated_programs)) {
181 
182                         @Override
183                         protected void onUpdate() {
184                             super.onUpdate();
185                             setChecked(
186                                     mParentalControlSettings.isRatingBlocked(
187                                             ImmutableList.of(TvContentRating.UNRATED)));
188                         }
189 
190                         @Override
191                         protected void onSelected() {
192                             super.onSelected();
193                             if (mParentalControlSettings.setUnratedBlocked(isChecked())) {
194                                 updateRatingLevels();
195                             }
196                         }
197                     };
198         } else {
199             mBlockUnratedItem = null;
200         }
201     }
202 
203     @Override
onResume()204     public void onResume() {
205         super.onResume();
206         // Although we set the attribution item at the end of the item list non-focusable, we do get
207         // its position when the fragment is resumed. This ensures that we do not select the
208         // non-focusable item at the end of the list. See b/17387103.
209         if (getSelectedPosition() >= mItemsSize) {
210             setSelectedPosition(mItemsSize - 1);
211         }
212     }
213 
updateRatingLevels()214     private void updateRatingLevels() {
215         @ContentRatingLevel int ratingLevel = mParentalControlSettings.getContentRatingLevel();
216         for (RatingLevelItem ratingLevelItem : mRatingLevelItems) {
217             ratingLevelItem.setChecked(ratingLevel == ratingLevelItem.mRatingLevel);
218         }
219     }
220 
updateDependentRatingItems( ContentRatingSystem.Order order, int selectedRatingOrderIndex, String contentRatingSystemId, boolean isChecked)221     private void updateDependentRatingItems(
222             ContentRatingSystem.Order order,
223             int selectedRatingOrderIndex,
224             String contentRatingSystemId,
225             boolean isChecked) {
226         List<RatingItem> ratingItems = mContentRatingSystemItemMap.get(contentRatingSystemId);
227         if (ratingItems != null) {
228             for (RatingItem item : ratingItems) {
229                 int ratingOrderIndex = item.getRatingOrderIndex(order);
230                 if (ratingOrderIndex != -1
231                         && ((ratingOrderIndex > selectedRatingOrderIndex && isChecked)
232                                 || (ratingOrderIndex < selectedRatingOrderIndex && !isChecked))) {
233                     item.setRatingBlocked(isChecked);
234                 }
235             }
236         }
237     }
238 
239     private class RatingLevelItem extends RadioButtonItem {
240         private final int mRatingLevel;
241 
RatingLevelItem(int ratingLevel)242         private RatingLevelItem(int ratingLevel) {
243             super(
244                     getString(sLevelResourceIdMap.get(ratingLevel)),
245                     (sDescriptionResourceIdMap.indexOfKey(ratingLevel) >= 0)
246                             ? getString(sDescriptionResourceIdMap.get(ratingLevel))
247                             : null);
248             mRatingLevel = ratingLevel;
249         }
250 
251         @Override
onSelected()252         protected void onSelected() {
253             super.onSelected();
254             mParentalControlSettings.setContentRatingLevel(
255                     getMainActivity().getContentRatingsManager(), mRatingLevel);
256             if (mBlockUnratedItem != null && mLegacyFlags.enableUnratedContentSettings()) {
257                 // set checked if UNRATED is blocked, and set unchecked otherwise.
258                 mBlockUnratedItem.setChecked(
259                         mParentalControlSettings.isRatingBlocked(
260                                 ImmutableList.of(TvContentRating.UNRATED)));
261             }
262             notifyItemsChanged(mRatingLevelItems.size());
263         }
264     }
265 
266     private class RatingItem extends CheckBoxItem {
267         protected final ContentRatingSystem mContentRatingSystem;
268         protected final Rating mRating;
269         private final Drawable mIcon;
270         private CompoundButton mCompoundButton;
271         private final List<ContentRatingSystem.Order> mOrders = new ArrayList<>();
272         private final List<Integer> mOrderIndexes = new ArrayList<>();
273 
RatingItem(ContentRatingSystem contentRatingSystem, Rating rating)274         private RatingItem(ContentRatingSystem contentRatingSystem, Rating rating) {
275             super(rating.getTitle(), rating.getDescription());
276             mContentRatingSystem = contentRatingSystem;
277             mRating = rating;
278             mIcon = rating.getIcon();
279             for (ContentRatingSystem.Order order : mContentRatingSystem.getOrders()) {
280                 int orderIndex = order.getRatingIndex(mRating);
281                 if (orderIndex != -1) {
282                     mOrders.add(order);
283                     mOrderIndexes.add(orderIndex);
284                 }
285             }
286         }
287 
288         @Override
onBind(View view)289         protected void onBind(View view) {
290             super.onBind(view);
291 
292             mCompoundButton = (CompoundButton) view.findViewById(getCompoundButtonId());
293             mCompoundButton.setVisibility(View.VISIBLE);
294 
295             ImageView imageView = (ImageView) view.findViewById(R.id.icon);
296             if (mIcon != null) {
297                 imageView.setVisibility(View.VISIBLE);
298                 imageView.setImageDrawable(mIcon);
299             } else {
300                 imageView.setVisibility(View.GONE);
301             }
302         }
303 
304         @Override
onUnbind()305         protected void onUnbind() {
306             super.onUnbind();
307             mCompoundButton = null;
308         }
309 
310         @Override
onUpdate()311         protected void onUpdate() {
312             super.onUpdate();
313             mCompoundButton.setButtonDrawable(getButtonDrawable());
314             setChecked(mParentalControlSettings.isRatingBlocked(mContentRatingSystem, mRating));
315         }
316 
317         @Override
onSelected()318         protected void onSelected() {
319             super.onSelected();
320             if (mParentalControlSettings.setRatingBlocked(
321                     mContentRatingSystem, mRating, isChecked())) {
322                 updateRatingLevels();
323             }
324             // Automatically check/uncheck dependent ratings.
325             for (int i = 0; i < mOrders.size(); i++) {
326                 updateDependentRatingItems(
327                         mOrders.get(i),
328                         mOrderIndexes.get(i),
329                         mContentRatingSystem.getId(),
330                         isChecked());
331             }
332         }
333 
334         @Override
getResourceId()335         protected int getResourceId() {
336             return R.layout.option_item_rating;
337         }
338 
getButtonDrawable()339         protected int getButtonDrawable() {
340             return R.drawable.btn_lock_material_anim;
341         }
342 
getRatingOrderIndex(ContentRatingSystem.Order order)343         private int getRatingOrderIndex(ContentRatingSystem.Order order) {
344             int orderIndex = mOrders.indexOf(order);
345             return orderIndex == -1 ? -1 : mOrderIndexes.get(orderIndex);
346         }
347 
setRatingBlocked(boolean isChecked)348         private void setRatingBlocked(boolean isChecked) {
349             if (isChecked() == isChecked) {
350                 return;
351             }
352             mParentalControlSettings.setRatingBlocked(mContentRatingSystem, mRating, isChecked);
353             notifyUpdated();
354         }
355     }
356 
357     private class RatingWithSubItem extends RatingItem {
RatingWithSubItem(ContentRatingSystem contentRatingSystem, Rating rating)358         private RatingWithSubItem(ContentRatingSystem contentRatingSystem, Rating rating) {
359             super(contentRatingSystem, rating);
360         }
361 
362         @Override
onSelected()363         protected void onSelected() {
364             getMainActivity()
365                     .getOverlayManager()
366                     .getSideFragmentManager()
367                     .show(SubRatingsFragment.create(mContentRatingSystem, mRating.getName()));
368         }
369 
370         @Override
getButtonDrawable()371         protected int getButtonDrawable() {
372             int blockedStatus =
373                     mParentalControlSettings.getBlockedStatus(mContentRatingSystem, mRating);
374             if (blockedStatus == ParentalControlSettings.RATING_BLOCKED) {
375                 return R.drawable.btn_lock_material;
376             } else if (blockedStatus == ParentalControlSettings.RATING_BLOCKED_PARTIAL) {
377                 return R.drawable.btn_partial_lock_material;
378             }
379             return R.drawable.btn_unlock_material;
380         }
381     }
382 
383     /** Opens a dialog showing the sources of the rating descriptions. */
384     public static class AttributionItem extends Item {
385         public static final String DIALOG_TAG = AttributionItem.class.getSimpleName();
386         public static final String TRACKER_LABEL = "Sources for content rating systems";
387         private final MainActivity mMainActivity;
388 
AttributionItem(MainActivity mainActivity)389         public AttributionItem(MainActivity mainActivity) {
390             mMainActivity = mainActivity;
391         }
392 
393         @Override
getResourceId()394         protected int getResourceId() {
395             return R.layout.option_item_attribution;
396         }
397 
398         @Override
onSelected()399         protected void onSelected() {
400             WebDialogFragment dialog =
401                     WebDialogFragment.newInstance(
402                             LicenseUtils.RATING_SOURCE_FILE,
403                             mMainActivity.getString(R.string.option_attribution),
404                             TRACKER_LABEL);
405             mMainActivity.getOverlayManager().showDialogFragment(DIALOG_TAG, dialog, false);
406         }
407     }
408 }
409