1 /*
2  * Copyright (C) 2021 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.permissioncontroller.permission.ui.handheld.v31;
18 
19 import android.Manifest;
20 import android.content.Context;
21 import android.content.res.Configuration;
22 import android.os.Build;
23 import android.util.AttributeSet;
24 import android.util.TypedValue;
25 import android.widget.TextView;
26 
27 import androidx.annotation.AttrRes;
28 import androidx.annotation.NonNull;
29 import androidx.annotation.Nullable;
30 import androidx.annotation.RequiresApi;
31 import androidx.annotation.StyleRes;
32 import androidx.preference.Preference;
33 import androidx.preference.PreferenceViewHolder;
34 
35 import com.android.permissioncontroller.R;
36 
37 import java.util.Arrays;
38 import java.util.HashMap;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Objects;
42 
43 /**
44  * A Preference for the permission usage graphic.
45  */
46 @RequiresApi(Build.VERSION_CODES.S)
47 public class PermissionUsageGraphicPreference extends Preference {
48 
49     /** Permission group to count mapping. */
50     private @NonNull Map<String, Integer> mUsages = new HashMap<>();
51 
52     /** Whether to show the "Other" category. */
53     private boolean mShowOtherCategory;
54     private boolean mIsNightMode;
55     /** True if we want to show usage data for the past 7 days; otherwise show for past 24h*/
56     private boolean mShow7Days;
57 
PermissionUsageGraphicPreference(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)58     public PermissionUsageGraphicPreference(@NonNull Context context, @Nullable AttributeSet attrs,
59             @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
60         super(context, attrs, defStyleAttr, defStyleRes);
61         init(context);
62     }
63 
PermissionUsageGraphicPreference(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)64     public PermissionUsageGraphicPreference(@NonNull Context context, @Nullable AttributeSet attrs,
65             @AttrRes int defStyleAttr) {
66         super(context, attrs, defStyleAttr);
67         init(context);
68     }
69 
PermissionUsageGraphicPreference(@onNull Context context, @Nullable AttributeSet attrs)70     public PermissionUsageGraphicPreference(@NonNull Context context,
71             @Nullable AttributeSet attrs) {
72         super(context, attrs);
73         init(context);
74     }
75 
PermissionUsageGraphicPreference(@onNull Context context, boolean show7Days)76     public PermissionUsageGraphicPreference(@NonNull Context context, boolean show7Days) {
77         this(context);
78         mShow7Days = show7Days;
79     }
80 
PermissionUsageGraphicPreference(@onNull Context context)81     public PermissionUsageGraphicPreference(@NonNull Context context) {
82         super(context);
83         init(context);
84     }
85 
init(Context context)86     private void init(Context context) {
87         Configuration configuration = context.getResources().getConfiguration();
88         mIsNightMode = (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK)
89                 == Configuration.UI_MODE_NIGHT_YES;
90         setLayoutResource(R.layout.permission_usage_graphic);
91         setSelectable(false);
92     }
93 
94     /** Sets permission group usages: map of group name to usage count. */
setUsages(Map<String, Integer> usages)95     public void setUsages(Map<String, Integer> usages) {
96         if (!Objects.equals(mUsages, usages)) {
97             mUsages = usages;
98             notifyChanged();
99         }
100     }
101 
102     /** Sets whether to show the "Other" category. */
setShowOtherCategory(boolean showOtherCategory)103     public void setShowOtherCategory(boolean showOtherCategory) {
104         if (mShowOtherCategory != showOtherCategory) {
105             mShowOtherCategory = showOtherCategory;
106             notifyChanged();
107         }
108     }
109 
110     @Override
onBindViewHolder(PreferenceViewHolder holder)111     public void onBindViewHolder(PreferenceViewHolder holder) {
112         super.onBindViewHolder(holder);
113         boolean isUsagesEmpty = isUsagesEmpty();
114 
115         CompositeCircleView ccv =
116                 (CompositeCircleView) holder.findViewById(R.id.composite_circle_view);
117         CompositeCircleViewLabeler ccvl = (CompositeCircleViewLabeler) holder.findViewById(
118                 R.id.composite_circle_view_labeler);
119 
120         // Set center text.
121         // TODO(b/176902658): Fix text appearance.
122         TextView centerLabel = new TextView(getContext());
123         centerLabel.setTextAlignment(TextView.TEXT_ALIGNMENT_CENTER);
124 
125         int privdashLabel = mShow7Days ? R.string.privdash_label_7d : R.string.privdash_label_24h;
126         centerLabel.setText(getContext().getString(privdashLabel));
127         centerLabel.setTextAppearance(R.style.PrivacyDashboardGraphicLabel);
128 
129         int colorCameraRes = mIsNightMode ? android.R.color.system_accent1_100 :
130                 R.color.privacy_dash_graphic_pref_light_camera;
131         int colorMicrophoneRes = mIsNightMode ? R.color.privacy_dash_graphic_pref_dark_mic :
132                 R.color.privacy_dash_graphic_pref_light_mic;
133         int colorLocationRes = mIsNightMode ? android.R.color.system_accent3_300 :
134                 R.color.privacy_dash_graphic_pref_light_location;
135         int colorOtherRes = mIsNightMode ? R.color.privacy_dash_graphic_pref_dark_others :
136                 R.color.privacy_dash_graphic_pref_light_others;
137 
138         // Sample colors.
139         final int colorCamera = getContext().getColor(colorCameraRes);
140         final int colorMicrophone = getContext().getColor(colorMicrophoneRes);
141         final int colorLocation = getContext().getColor(colorLocationRes);
142         final int colorOther = getContext().getColor(colorOtherRes);
143 
144         // Create labels, counts, and colors.
145         TextView[] labels;
146         int[] counts;
147         int[] colors;
148         if (isUsagesEmpty) {
149             // Special case if usages are empty.
150             labels = new TextView[] { new TextView(getContext()) };
151             labels[0] = null;
152             counts = new int[] { 1 };
153             colors = new int[] { colorOther };
154         } else {
155             labels = new TextView[] {
156                     new TextView(getContext()),
157                     new TextView(getContext()),
158                     new TextView(getContext()),
159                     new TextView(getContext())
160             };
161             labels[0].setText(getContext().getString(R.string.privdash_label_camera));
162             labels[1].setText(getContext().getString(R.string.privdash_label_microphone));
163             labels[2].setText(getContext().getString(R.string.privdash_label_location));
164             labels[3].setText(getContext().getString(R.string.privdash_label_other));
165             counts = new int[] {
166                     getUsageCount(Manifest.permission_group.CAMERA),
167                     getUsageCount(Manifest.permission_group.MICROPHONE),
168                     getUsageCount(Manifest.permission_group.LOCATION),
169                     mShowOtherCategory
170                             ? getUsageCountExcluding(Manifest.permission_group.CAMERA,
171                             Manifest.permission_group.MICROPHONE,
172                             Manifest.permission_group.LOCATION) : 0
173             };
174             colors = new int[] {
175                     colorCamera,
176                     colorMicrophone,
177                     colorLocation,
178                     colorOther
179             };
180         }
181 
182         // Set label styles.
183         for (int i = 0; i < labels.length; i++) {
184             if (labels[i] != null) {
185                 labels[i].setTextAppearance(R.style.PrivacyDashboardGraphicLabel);
186             }
187         }
188 
189         // Get circle-related dimensions.
190         TypedValue outValue = new TypedValue();
191         getContext().getResources().getValue(R.dimen.privhub_label_radius_scalar,
192                 outValue, true);
193         float labelRadiusScalar = outValue.getFloat();
194         int circleStrokeWidth = (int) getContext().getResources().getDimension(
195                 R.dimen.privhub_circle_stroke_width);
196 
197         // Configure circle and labeler.
198         ccvl.configure(R.id.composite_circle_view, centerLabel, labels, labelRadiusScalar);
199         // Start at angle 300 (top right) to allow for small segments for cam, mic, and loc.
200         ccv.configure(300, counts, colors, circleStrokeWidth, labels);
201     }
202 
getUsageCount(String group)203     private int getUsageCount(String group) {
204         Integer count = mUsages.get(group);
205         if (count == null) {
206             return 0;
207         }
208         return count;
209     }
210 
getUsageCountExcluding(String... excludeGroups)211     private int getUsageCountExcluding(String... excludeGroups) {
212         int count = 0;
213         List<String> exclude = Arrays.asList(excludeGroups);
214         for (Map.Entry<String, Integer> entry : mUsages.entrySet()) {
215             if (exclude.indexOf(entry.getKey()) >= 0) {
216                 continue;
217             }
218             count += entry.getValue();
219         }
220         return count;
221     }
222 
isUsagesEmpty()223     private boolean isUsagesEmpty() {
224         return getUsageCountExcluding() == 0;
225     }
226 }
227