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