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