1 /* 2 * Copyright (C) 2019 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.car.developeroptions.privacy; 18 19 import static android.Manifest.permission_group.CAMERA; 20 import static android.Manifest.permission_group.LOCATION; 21 import static android.Manifest.permission_group.MICROPHONE; 22 23 import static java.util.concurrent.TimeUnit.DAYS; 24 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.PackageManager; 28 import android.graphics.drawable.Drawable; 29 import android.os.Bundle; 30 import android.permission.PermissionControllerManager; 31 import android.permission.RuntimePermissionUsageInfo; 32 import android.provider.DeviceConfig; 33 import android.util.Log; 34 import android.view.View; 35 36 import androidx.annotation.NonNull; 37 import androidx.annotation.VisibleForTesting; 38 import androidx.preference.PreferenceScreen; 39 40 import com.android.car.developeroptions.R; 41 import com.android.car.developeroptions.core.BasePreferenceController; 42 import com.android.settingslib.Utils; 43 import com.android.settingslib.core.lifecycle.LifecycleObserver; 44 import com.android.settingslib.core.lifecycle.events.OnCreate; 45 import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; 46 import com.android.settingslib.core.lifecycle.events.OnStart; 47 import com.android.settingslib.widget.BarChartInfo; 48 import com.android.settingslib.widget.BarChartPreference; 49 import com.android.settingslib.widget.BarViewInfo; 50 51 import java.util.ArrayList; 52 import java.util.List; 53 54 55 public class PermissionBarChartPreferenceController extends BasePreferenceController implements 56 PermissionControllerManager.OnPermissionUsageResultCallback, LifecycleObserver, OnCreate, 57 OnStart, OnSaveInstanceState { 58 59 private static final String TAG = "BarChartPreferenceCtl"; 60 private static final String KEY_PERMISSION_USAGE = "usage_infos"; 61 62 @VisibleForTesting 63 List<RuntimePermissionUsageInfo> mOldUsageInfos; 64 private PackageManager mPackageManager; 65 private PrivacyDashboardFragment mParent; 66 private BarChartPreference mBarChartPreference; 67 PermissionBarChartPreferenceController(Context context, String preferenceKey)68 public PermissionBarChartPreferenceController(Context context, String preferenceKey) { 69 super(context, preferenceKey); 70 mOldUsageInfos = new ArrayList<>(); 71 mPackageManager = context.getPackageManager(); 72 } 73 setFragment(PrivacyDashboardFragment fragment)74 public void setFragment(PrivacyDashboardFragment fragment) { 75 mParent = fragment; 76 } 77 78 @Override onCreate(Bundle savedInstanceState)79 public void onCreate(Bundle savedInstanceState) { 80 if (savedInstanceState != null) { 81 mOldUsageInfos = savedInstanceState.getParcelableArrayList(KEY_PERMISSION_USAGE); 82 } 83 } 84 85 @Override onSaveInstanceState(Bundle outState)86 public void onSaveInstanceState(Bundle outState) { 87 outState.putParcelableList(KEY_PERMISSION_USAGE, mOldUsageInfos); 88 } 89 90 @Override getAvailabilityStatus()91 public int getAvailabilityStatus() { 92 return Boolean.parseBoolean( 93 DeviceConfig.getProperty(DeviceConfig.NAMESPACE_PRIVACY, 94 com.android.car.developeroptions.Utils.PROPERTY_PERMISSIONS_HUB_ENABLED)) ? 95 AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; 96 } 97 98 @Override displayPreference(PreferenceScreen screen)99 public void displayPreference(PreferenceScreen screen) { 100 super.displayPreference(screen); 101 mBarChartPreference = screen.findPreference(getPreferenceKey()); 102 103 final BarChartInfo info = new BarChartInfo.Builder() 104 .setTitle(R.string.permission_bar_chart_title) 105 .setDetails(R.string.permission_bar_chart_details) 106 .setEmptyText(R.string.permission_bar_chart_empty_text) 107 .setDetailsOnClickListener((View v) -> { 108 final Intent intent = new Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE); 109 intent.putExtra(Intent.EXTRA_DURATION_MILLIS, DAYS.toMillis(1)); 110 mContext.startActivity(intent); 111 }) 112 .build(); 113 114 mBarChartPreference.initializeBarChart(info); 115 if (!mOldUsageInfos.isEmpty()) { 116 mBarChartPreference.setBarViewInfos(createBarViews(mOldUsageInfos)); 117 } 118 } 119 120 @Override onStart()121 public void onStart() { 122 if (!isAvailable()) { 123 return; 124 } 125 126 // We don't hide chart when we have existing data. 127 mBarChartPreference.updateLoadingState(mOldUsageInfos.isEmpty() /* isLoading */); 128 // But we still need to hint user with progress bar that we are updating new usage data. 129 mParent.setLoadingEnabled(true /* enabled */); 130 retrievePermissionUsageData(); 131 } 132 133 @Override onPermissionUsageResult(@onNull List<RuntimePermissionUsageInfo> usageInfos)134 public void onPermissionUsageResult(@NonNull List<RuntimePermissionUsageInfo> usageInfos) { 135 usageInfos.sort((x, y) -> { 136 int usageDiff = y.getAppAccessCount() - x.getAppAccessCount(); 137 if (usageDiff != 0) { 138 return usageDiff; 139 } 140 String xName = x.getName(); 141 String yName = y.getName(); 142 if (xName.equals(LOCATION)) { 143 return -1; 144 } else if (yName.equals(LOCATION)) { 145 return 1; 146 } else if (xName.equals(MICROPHONE)) { 147 return -1; 148 } else if (yName.equals(MICROPHONE)) { 149 return 1; 150 } else if (xName.equals(CAMERA)) { 151 return -1; 152 } else if (yName.equals(CAMERA)) { 153 return 1; 154 } 155 return x.getName().compareTo(y.getName()); 156 }); 157 158 // If the result is different, we need to update bar views. 159 if (!areSamePermissionGroups(usageInfos)) { 160 mBarChartPreference.setBarViewInfos(createBarViews(usageInfos)); 161 mOldUsageInfos = usageInfos; 162 } 163 164 mBarChartPreference.updateLoadingState(false /* isLoading */); 165 mParent.setLoadingEnabled(false /* enabled */); 166 } 167 retrievePermissionUsageData()168 private void retrievePermissionUsageData() { 169 mContext.getSystemService(PermissionControllerManager.class).getPermissionUsages( 170 false /* countSystem */, (int) DAYS.toMillis(1), 171 mContext.getMainExecutor() /* executor */, this /* callback */); 172 } 173 createBarViews(List<RuntimePermissionUsageInfo> usageInfos)174 private BarViewInfo[] createBarViews(List<RuntimePermissionUsageInfo> usageInfos) { 175 if (usageInfos.isEmpty()) { 176 return null; 177 } 178 179 // STOPSHIP: Ignore the STORAGE group since it's going away. 180 usageInfos.removeIf(usage -> usage.getName().equals("android.permission-group.STORAGE")); 181 182 final BarViewInfo[] barViewInfos = new BarViewInfo[ 183 Math.min(BarChartPreference.MAXIMUM_BAR_VIEWS, usageInfos.size())]; 184 185 for (int index = 0; index < barViewInfos.length; index++) { 186 final RuntimePermissionUsageInfo permissionGroupInfo = usageInfos.get(index); 187 final int count = permissionGroupInfo.getAppAccessCount(); 188 final CharSequence permLabel = getPermissionGroupLabel(permissionGroupInfo.getName()); 189 190 barViewInfos[index] = new BarViewInfo( 191 getPermissionGroupIcon(permissionGroupInfo.getName()), count, permLabel, 192 mContext.getResources().getQuantityString(R.plurals.permission_bar_chart_label, 193 count, count), permLabel); 194 195 // Set the click listener for each bar view. 196 // The listener will navigate user to permission usage app. 197 barViewInfos[index].setClickListener((View v) -> { 198 final Intent intent = new Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE); 199 intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permissionGroupInfo.getName()); 200 intent.putExtra(Intent.EXTRA_DURATION_MILLIS, DAYS.toMillis(1)); 201 mContext.startActivity(intent); 202 }); 203 } 204 205 return barViewInfos; 206 } 207 getPermissionGroupIcon(String permissionGroup)208 private Drawable getPermissionGroupIcon(String permissionGroup) { 209 Drawable icon = null; 210 try { 211 icon = mPackageManager.getPermissionGroupInfo(permissionGroup, 0) 212 .loadIcon(mPackageManager); 213 icon.setTintList(Utils.getColorAttr(mContext, android.R.attr.textColorSecondary)); 214 } catch (PackageManager.NameNotFoundException e) { 215 Log.w(TAG, "Cannot find group icon for " + permissionGroup, e); 216 } 217 218 return icon; 219 } 220 getPermissionGroupLabel(String permissionGroup)221 private CharSequence getPermissionGroupLabel(String permissionGroup) { 222 CharSequence label = null; 223 try { 224 label = mPackageManager.getPermissionGroupInfo(permissionGroup, 0) 225 .loadLabel(mPackageManager); 226 } catch (PackageManager.NameNotFoundException e) { 227 Log.w(TAG, "Cannot find group label for " + permissionGroup, e); 228 } 229 230 return label; 231 } 232 areSamePermissionGroups(List<RuntimePermissionUsageInfo> newUsageInfos)233 private boolean areSamePermissionGroups(List<RuntimePermissionUsageInfo> newUsageInfos) { 234 if (newUsageInfos.size() != mOldUsageInfos.size()) { 235 return false; 236 } 237 238 for (int index = 0; index < newUsageInfos.size(); index++) { 239 final RuntimePermissionUsageInfo newInfo = newUsageInfos.get(index); 240 final RuntimePermissionUsageInfo oldInfo = mOldUsageInfos.get(index); 241 242 if (!newInfo.getName().equals(oldInfo.getName()) || 243 newInfo.getAppAccessCount() != oldInfo.getAppAccessCount()) { 244 return false; 245 } 246 } 247 return true; 248 } 249 } 250