/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.privacy; import static android.Manifest.permission_group.CAMERA; import static android.Manifest.permission_group.LOCATION; import static android.Manifest.permission_group.MICROPHONE; import static java.util.concurrent.TimeUnit.DAYS; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.permission.PermissionControllerManager; import android.permission.RuntimePermissionUsageInfo; import android.provider.DeviceConfig; import android.util.Log; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settingslib.Utils; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnCreate; import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.widget.BarChartInfo; import com.android.settingslib.widget.BarChartPreference; import com.android.settingslib.widget.BarViewInfo; import java.util.ArrayList; import java.util.List; public class PermissionBarChartPreferenceController extends BasePreferenceController implements PermissionControllerManager.OnPermissionUsageResultCallback, LifecycleObserver, OnCreate, OnStart, OnSaveInstanceState { private static final String TAG = "BarChartPreferenceCtl"; private static final String KEY_PERMISSION_USAGE = "usage_infos"; @VisibleForTesting List mOldUsageInfos; private PackageManager mPackageManager; private PrivacyDashboardFragment mParent; private BarChartPreference mBarChartPreference; public PermissionBarChartPreferenceController(Context context, String preferenceKey) { super(context, preferenceKey); mOldUsageInfos = new ArrayList<>(); mPackageManager = context.getPackageManager(); } public void setFragment(PrivacyDashboardFragment fragment) { mParent = fragment; } @Override public void onCreate(Bundle savedInstanceState) { if (savedInstanceState != null) { mOldUsageInfos = savedInstanceState.getParcelableArrayList(KEY_PERMISSION_USAGE); } } @Override public void onSaveInstanceState(Bundle outState) { outState.putParcelableList(KEY_PERMISSION_USAGE, mOldUsageInfos); } @Override public int getAvailabilityStatus() { return UNSUPPORTED_ON_DEVICE; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); mBarChartPreference = screen.findPreference(getPreferenceKey()); final BarChartInfo info = new BarChartInfo.Builder() .setTitle(R.string.permission_bar_chart_title) .setDetails(R.string.permission_bar_chart_details) .setEmptyText(R.string.permission_bar_chart_empty_text) .setDetailsOnClickListener((View v) -> { final Intent intent = new Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE); intent.putExtra(Intent.EXTRA_DURATION_MILLIS, DAYS.toMillis(1)); mContext.startActivity(intent); }) .build(); mBarChartPreference.initializeBarChart(info); if (!mOldUsageInfos.isEmpty()) { mBarChartPreference.setBarViewInfos(createBarViews(mOldUsageInfos)); } } @Override public void onStart() { if (!isAvailable()) { return; } // We don't hide chart when we have existing data. mBarChartPreference.updateLoadingState(mOldUsageInfos.isEmpty() /* isLoading */); // But we still need to hint user with progress bar that we are updating new usage data. mParent.setLoadingEnabled(true /* enabled */); retrievePermissionUsageData(); } @Override public void onPermissionUsageResult(@NonNull List usageInfos) { usageInfos.sort((x, y) -> { int usageDiff = y.getAppAccessCount() - x.getAppAccessCount(); if (usageDiff != 0) { return usageDiff; } String xName = x.getName(); String yName = y.getName(); if (xName.equals(LOCATION)) { return -1; } else if (yName.equals(LOCATION)) { return 1; } else if (xName.equals(MICROPHONE)) { return -1; } else if (yName.equals(MICROPHONE)) { return 1; } else if (xName.equals(CAMERA)) { return -1; } else if (yName.equals(CAMERA)) { return 1; } return x.getName().compareTo(y.getName()); }); // If the result is different, we need to update bar views. if (!areSamePermissionGroups(usageInfos)) { mBarChartPreference.setBarViewInfos(createBarViews(usageInfos)); mOldUsageInfos = usageInfos; } mBarChartPreference.updateLoadingState(false /* isLoading */); mParent.setLoadingEnabled(false /* enabled */); } private void retrievePermissionUsageData() { mContext.getSystemService(PermissionControllerManager.class).getPermissionUsages( false /* countSystem */, (int) DAYS.toMillis(1), mContext.getMainExecutor() /* executor */, this /* callback */); } private BarViewInfo[] createBarViews(List usageInfos) { if (usageInfos.isEmpty()) { return null; } final BarViewInfo[] barViewInfos = new BarViewInfo[ Math.min(BarChartPreference.MAXIMUM_BAR_VIEWS, usageInfos.size())]; for (int index = 0; index < barViewInfos.length; index++) { final RuntimePermissionUsageInfo permissionGroupInfo = usageInfos.get(index); final int count = permissionGroupInfo.getAppAccessCount(); final CharSequence permLabel = getPermissionGroupLabel(permissionGroupInfo.getName()); barViewInfos[index] = new BarViewInfo( getPermissionGroupIcon(permissionGroupInfo.getName()), count, permLabel, mContext.getResources().getQuantityString(R.plurals.permission_bar_chart_label, count, count), permLabel); // Set the click listener for each bar view. // The listener will navigate user to permission usage app. barViewInfos[index].setClickListener((View v) -> { final Intent intent = new Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE); intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permissionGroupInfo.getName()); intent.putExtra(Intent.EXTRA_DURATION_MILLIS, DAYS.toMillis(1)); mContext.startActivity(intent); }); } return barViewInfos; } private Drawable getPermissionGroupIcon(String permissionGroup) { Drawable icon = null; try { icon = mPackageManager.getPermissionGroupInfo(permissionGroup, 0) .loadIcon(mPackageManager); icon.setTintList(Utils.getColorAttr(mContext, android.R.attr.textColorSecondary)); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "Cannot find group icon for " + permissionGroup, e); } return icon; } private CharSequence getPermissionGroupLabel(String permissionGroup) { CharSequence label = null; try { label = mPackageManager.getPermissionGroupInfo(permissionGroup, 0) .loadLabel(mPackageManager); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "Cannot find group label for " + permissionGroup, e); } return label; } private boolean areSamePermissionGroups(List newUsageInfos) { if (newUsageInfos.size() != mOldUsageInfos.size()) { return false; } for (int index = 0; index < newUsageInfos.size(); index++) { final RuntimePermissionUsageInfo newInfo = newUsageInfos.get(index); final RuntimePermissionUsageInfo oldInfo = mOldUsageInfos.get(index); if (!newInfo.getName().equals(oldInfo.getName()) || newInfo.getAppAccessCount() != oldInfo.getAppAccessCount()) { return false; } } return true; } }