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