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.settings.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.settings.R; 41 import com.android.settings.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 UNSUPPORTED_ON_DEVICE; 93 } 94 95 @Override displayPreference(PreferenceScreen screen)96 public void displayPreference(PreferenceScreen screen) { 97 super.displayPreference(screen); 98 mBarChartPreference = screen.findPreference(getPreferenceKey()); 99 100 final BarChartInfo info = new BarChartInfo.Builder() 101 .setTitle(R.string.permission_bar_chart_title) 102 .setDetails(R.string.permission_bar_chart_details) 103 .setEmptyText(R.string.permission_bar_chart_empty_text) 104 .setDetailsOnClickListener((View v) -> { 105 final Intent intent = new Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE); 106 intent.putExtra(Intent.EXTRA_DURATION_MILLIS, DAYS.toMillis(1)); 107 mContext.startActivity(intent); 108 }) 109 .build(); 110 111 mBarChartPreference.initializeBarChart(info); 112 if (!mOldUsageInfos.isEmpty()) { 113 mBarChartPreference.setBarViewInfos(createBarViews(mOldUsageInfos)); 114 } 115 } 116 117 @Override onStart()118 public void onStart() { 119 if (!isAvailable()) { 120 return; 121 } 122 123 // We don't hide chart when we have existing data. 124 mBarChartPreference.updateLoadingState(mOldUsageInfos.isEmpty() /* isLoading */); 125 // But we still need to hint user with progress bar that we are updating new usage data. 126 mParent.setLoadingEnabled(true /* enabled */); 127 retrievePermissionUsageData(); 128 } 129 130 @Override onPermissionUsageResult(@onNull List<RuntimePermissionUsageInfo> usageInfos)131 public void onPermissionUsageResult(@NonNull List<RuntimePermissionUsageInfo> usageInfos) { 132 usageInfos.sort((x, y) -> { 133 int usageDiff = y.getAppAccessCount() - x.getAppAccessCount(); 134 if (usageDiff != 0) { 135 return usageDiff; 136 } 137 String xName = x.getName(); 138 String yName = y.getName(); 139 if (xName.equals(LOCATION)) { 140 return -1; 141 } else if (yName.equals(LOCATION)) { 142 return 1; 143 } else if (xName.equals(MICROPHONE)) { 144 return -1; 145 } else if (yName.equals(MICROPHONE)) { 146 return 1; 147 } else if (xName.equals(CAMERA)) { 148 return -1; 149 } else if (yName.equals(CAMERA)) { 150 return 1; 151 } 152 return x.getName().compareTo(y.getName()); 153 }); 154 155 // If the result is different, we need to update bar views. 156 if (!areSamePermissionGroups(usageInfos)) { 157 mBarChartPreference.setBarViewInfos(createBarViews(usageInfos)); 158 mOldUsageInfos = usageInfos; 159 } 160 161 mBarChartPreference.updateLoadingState(false /* isLoading */); 162 mParent.setLoadingEnabled(false /* enabled */); 163 } 164 retrievePermissionUsageData()165 private void retrievePermissionUsageData() { 166 mContext.getSystemService(PermissionControllerManager.class).getPermissionUsages( 167 false /* countSystem */, (int) DAYS.toMillis(1), 168 mContext.getMainExecutor() /* executor */, this /* callback */); 169 } 170 createBarViews(List<RuntimePermissionUsageInfo> usageInfos)171 private BarViewInfo[] createBarViews(List<RuntimePermissionUsageInfo> usageInfos) { 172 if (usageInfos.isEmpty()) { 173 return null; 174 } 175 176 final BarViewInfo[] barViewInfos = new BarViewInfo[ 177 Math.min(BarChartPreference.MAXIMUM_BAR_VIEWS, usageInfos.size())]; 178 179 for (int index = 0; index < barViewInfos.length; index++) { 180 final RuntimePermissionUsageInfo permissionGroupInfo = usageInfos.get(index); 181 final int count = permissionGroupInfo.getAppAccessCount(); 182 final CharSequence permLabel = getPermissionGroupLabel(permissionGroupInfo.getName()); 183 184 barViewInfos[index] = new BarViewInfo( 185 getPermissionGroupIcon(permissionGroupInfo.getName()), count, permLabel, 186 mContext.getResources().getQuantityString(R.plurals.permission_bar_chart_label, 187 count, count), permLabel); 188 189 // Set the click listener for each bar view. 190 // The listener will navigate user to permission usage app. 191 barViewInfos[index].setClickListener((View v) -> { 192 final Intent intent = new Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE); 193 intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permissionGroupInfo.getName()); 194 intent.putExtra(Intent.EXTRA_DURATION_MILLIS, DAYS.toMillis(1)); 195 mContext.startActivity(intent); 196 }); 197 } 198 199 return barViewInfos; 200 } 201 getPermissionGroupIcon(String permissionGroup)202 private Drawable getPermissionGroupIcon(String permissionGroup) { 203 Drawable icon = null; 204 try { 205 icon = mPackageManager.getPermissionGroupInfo(permissionGroup, 0) 206 .loadIcon(mPackageManager); 207 icon.setTintList(Utils.getColorAttr(mContext, android.R.attr.textColorSecondary)); 208 } catch (PackageManager.NameNotFoundException e) { 209 Log.w(TAG, "Cannot find group icon for " + permissionGroup, e); 210 } 211 212 return icon; 213 } 214 getPermissionGroupLabel(String permissionGroup)215 private CharSequence getPermissionGroupLabel(String permissionGroup) { 216 CharSequence label = null; 217 try { 218 label = mPackageManager.getPermissionGroupInfo(permissionGroup, 0) 219 .loadLabel(mPackageManager); 220 } catch (PackageManager.NameNotFoundException e) { 221 Log.w(TAG, "Cannot find group label for " + permissionGroup, e); 222 } 223 224 return label; 225 } 226 areSamePermissionGroups(List<RuntimePermissionUsageInfo> newUsageInfos)227 private boolean areSamePermissionGroups(List<RuntimePermissionUsageInfo> newUsageInfos) { 228 if (newUsageInfos.size() != mOldUsageInfos.size()) { 229 return false; 230 } 231 232 for (int index = 0; index < newUsageInfos.size(); index++) { 233 final RuntimePermissionUsageInfo newInfo = newUsageInfos.get(index); 234 final RuntimePermissionUsageInfo oldInfo = mOldUsageInfos.get(index); 235 236 if (!newInfo.getName().equals(oldInfo.getName()) || 237 newInfo.getAppAccessCount() != oldInfo.getAppAccessCount()) { 238 return false; 239 } 240 } 241 return true; 242 } 243 } 244