| /* |
| * 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.permissioncontroller.permission.ui.auto; |
| |
| import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID; |
| import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID; |
| import static com.android.permissioncontroller.permission.ui.Category.ALLOWED; |
| import static com.android.permissioncontroller.permission.ui.Category.ALLOWED_FOREGROUND; |
| import static com.android.permissioncontroller.permission.ui.Category.ASK; |
| import static com.android.permissioncontroller.permission.ui.Category.DENIED; |
| import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_CALLER_NAME; |
| |
| import android.content.Context; |
| import android.content.Intent; |
| import android.graphics.drawable.Drawable; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.UserHandle; |
| import android.util.ArrayMap; |
| |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RequiresApi; |
| import androidx.fragment.app.Fragment; |
| import androidx.lifecycle.ViewModelProvider; |
| import androidx.preference.Preference; |
| import androidx.preference.PreferenceCategory; |
| |
| import com.android.modules.utils.build.SdkLevel; |
| import com.android.permissioncontroller.R; |
| import com.android.permissioncontroller.auto.AutoSettingsFrameFragment; |
| import com.android.permissioncontroller.permission.model.AppPermissionUsage; |
| import com.android.permissioncontroller.permission.model.PermissionUsages; |
| import com.android.permissioncontroller.permission.ui.Category; |
| import com.android.permissioncontroller.permission.ui.handheld.SmartIconLoadPackagePermissionPreference; |
| import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel; |
| import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModelFactory; |
| import com.android.permissioncontroller.permission.utils.KotlinUtils; |
| import com.android.permissioncontroller.permission.utils.Utils; |
| import com.android.settingslib.utils.applications.AppUtils; |
| |
| import java.text.Collator; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Random; |
| |
| import kotlin.Pair; |
| |
| /** Shows the list of applications which have (or do not have) the given permission. */ |
| public class AutoPermissionAppsFragment extends AutoSettingsFrameFragment implements |
| PermissionUsages.PermissionsUsagesChangeCallback { |
| |
| private static final String LOG_TAG = "AutoPermissionAppsFragment"; |
| private static final String KEY_EMPTY = "_empty"; |
| |
| /** Creates a new instance of {@link AutoPermissionAppsFragment} for the given permission. */ |
| public static AutoPermissionAppsFragment newInstance(String permissionName, long sessionId) { |
| return setPermissionName(new AutoPermissionAppsFragment(), permissionName, sessionId); |
| } |
| |
| private static <T extends Fragment> T setPermissionName( |
| T fragment, String permissionName, long sessionId) { |
| Bundle arguments = new Bundle(); |
| arguments.putString(Intent.EXTRA_PERMISSION_NAME, permissionName); |
| arguments.putLong(EXTRA_SESSION_ID, sessionId); |
| fragment.setArguments(arguments); |
| return fragment; |
| } |
| |
| private PermissionAppsViewModel mViewModel; |
| private PermissionUsages mPermissionUsages; |
| private List<AppPermissionUsage> mAppPermissionUsages = new ArrayList<>(); |
| private String mPermGroupName; |
| |
| private Collator mCollator; |
| |
| @Override |
| public void onCreate(@Nullable Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| setLoading(true); |
| |
| mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME); |
| if (mPermGroupName == null) { |
| mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_NAME); |
| } |
| |
| Drawable icon = KotlinUtils.INSTANCE.getPermGroupIcon(getContext(), mPermGroupName); |
| CharSequence label = KotlinUtils.INSTANCE.getPermGroupLabel(getContext(), mPermGroupName); |
| CharSequence description = KotlinUtils.INSTANCE.getPermGroupDescription(getContext(), |
| mPermGroupName); |
| |
| setHeaderLabel(label); |
| Preference header = new Preference(getContext()); |
| header.setTitle(label); |
| header.setIcon(icon); |
| header.setSummary(Utils.getPermissionGroupDescriptionString(getContext(), mPermGroupName, |
| description)); |
| getPreferenceScreen().addPreference(header); |
| |
| mCollator = Collator.getInstance( |
| getContext().getResources().getConfiguration().getLocales().get(0)); |
| |
| PermissionAppsViewModelFactory factory = |
| new PermissionAppsViewModelFactory(getActivity().getApplication(), mPermGroupName, |
| this, new Bundle()); |
| mViewModel = new ViewModelProvider(this, factory).get(PermissionAppsViewModel.class); |
| |
| mViewModel.getCategorizedAppsLiveData().observe(this, this::onPackagesLoaded); |
| mViewModel.getShouldShowSystemLiveData().observe(this, this::updateMenu); |
| mViewModel.getHasSystemAppsLiveData().observe(this, this::hideSystemAppToggleIfNecessary); |
| |
| // If the build type is below S, the app ops for permission usage can't be found. Thus, we |
| // shouldn't load permission usages, for them. |
| if (SdkLevel.isAtLeastS()) { |
| Context context = getPreferenceManager().getContext(); |
| mPermissionUsages = new PermissionUsages(context); |
| |
| long filterTimeBeginMillis = mViewModel.getFilterTimeBeginMillis(); |
| mPermissionUsages.load(null, null, filterTimeBeginMillis, Long.MAX_VALUE, |
| PermissionUsages.USAGE_FLAG_LAST, getActivity().getLoaderManager(), |
| false, false, this, false); |
| } |
| } |
| |
| private void updateMenu(Boolean showSystem) { |
| if (showSystem == null) { |
| showSystem = false; |
| } |
| // Show the opposite label from the current state. |
| String label; |
| if (showSystem) { |
| label = getString(R.string.menu_hide_system); |
| } else { |
| label = getString(R.string.menu_show_system); |
| } |
| |
| boolean showSystemFinal = showSystem; |
| setAction(label, v -> mViewModel.updateShowSystem(!showSystemFinal)); |
| } |
| |
| /** |
| * Main differences between this phone implementation and this one are: |
| * <ul> |
| * <li>No special handling for scoped storage</li> |
| * </ul> |
| */ |
| private void onPackagesLoaded(Map<Category, List<Pair<String, UserHandle>>> categories) { |
| Preference additionalPermissionsPreference = getPreferenceScreen().findPreference( |
| ALLOWED_FOREGROUND.getCategoryName()); |
| if (additionalPermissionsPreference == null) { |
| // This preference resources includes the "Ask" permission group. That's okay for Auto |
| // even though Auto doesn't support the one-time permission because the code later in |
| // this method will hide unused permission groups. |
| addPreferencesFromResource(R.xml.allowed_denied); |
| } |
| // Hide allowed foreground label by default, to avoid briefly showing it before updating |
| findPreference(ALLOWED_FOREGROUND.getCategoryName()).setVisible(false); |
| Context context = getPreferenceManager().getContext(); |
| |
| if (context == null || getActivity() == null || categories == null) { |
| return; |
| } |
| |
| Map<String, Preference> existingPrefs = new ArrayMap<>(); |
| |
| // Start at 1 since the header preference will always be in the 0th index |
| for (int i = 1; i < getPreferenceScreen().getPreferenceCount(); i++) { |
| PreferenceCategory category = (PreferenceCategory) |
| getPreferenceScreen().getPreference(i); |
| category.setOrderingAsAdded(true); |
| int numPreferences = category.getPreferenceCount(); |
| for (int j = 0; j < numPreferences; j++) { |
| Preference preference = category.getPreference(j); |
| existingPrefs.put(preference.getKey(), preference); |
| } |
| category.removeAll(); |
| } |
| |
| long viewIdForLogging = new Random().nextLong(); |
| long sessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID); |
| |
| Boolean showAlways = mViewModel.getShowAllowAlwaysStringLiveData().getValue(); |
| if (showAlways != null && showAlways) { |
| findPreference(ALLOWED.getCategoryName()).setTitle(R.string.allowed_always_header); |
| } else { |
| findPreference(ALLOWED.getCategoryName()).setTitle(R.string.allowed_header); |
| } |
| |
| // A mapping of user + packageName to their last access timestamps for the permission group. |
| Map<String, Long> groupUsageLastAccessTime = |
| mViewModel.extractGroupUsageLastAccessTime(mAppPermissionUsages); |
| |
| for (Category grantCategory : categories.keySet()) { |
| List<Pair<String, UserHandle>> packages = categories.get(grantCategory); |
| PreferenceCategory category = findPreference(grantCategory.getCategoryName()); |
| |
| // If this category is empty set up the empty preference. |
| if (packages.size() == 0) { |
| Preference empty = new Preference(context); |
| empty.setSelectable(false); |
| empty.setKey(category.getKey() + KEY_EMPTY); |
| if (grantCategory.equals(ALLOWED)) { |
| empty.setTitle(getString(R.string.no_apps_allowed)); |
| } else if (grantCategory.equals(ALLOWED_FOREGROUND)) { |
| category.setVisible(false); |
| } else if (grantCategory.equals(ASK)) { |
| category.setVisible(false); |
| } else { |
| empty.setTitle(getString(R.string.no_apps_denied)); |
| } |
| category.addPreference(empty); |
| continue; |
| } else if (grantCategory.equals(ALLOWED_FOREGROUND)) { |
| category.setVisible(true); |
| } else if (grantCategory.equals(ASK)) { |
| category.setVisible(true); |
| } |
| |
| for (Pair<String, UserHandle> packageUserLabel : packages) { |
| String packageName = packageUserLabel.getFirst(); |
| UserHandle user = packageUserLabel.getSecond(); |
| |
| String key = user + packageName; |
| |
| Long lastAccessTime = groupUsageLastAccessTime.get(key); |
| Pair<String, Integer> summaryTimestamp = Utils |
| .getPermissionLastAccessSummaryTimestamp( |
| lastAccessTime, context, mPermGroupName); |
| |
| Preference existingPref = existingPrefs.get(key); |
| if (existingPref != null) { |
| updatePreferenceSummary(existingPref, summaryTimestamp); |
| category.addPreference(existingPref); |
| continue; |
| } |
| |
| SmartIconLoadPackagePermissionPreference pref = |
| new SmartIconLoadPackagePermissionPreference(getActivity().getApplication(), |
| packageName, user, context); |
| pref.setKey(key); |
| pref.setTitle(KotlinUtils.INSTANCE.getPackageLabel(getActivity().getApplication(), |
| packageName, user)); |
| pref.setOnPreferenceClickListener((Preference p) -> { |
| Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSION); |
| intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); |
| intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, mPermGroupName); |
| intent.putExtra(Intent.EXTRA_USER, user); |
| intent.putExtra(EXTRA_CALLER_NAME, getClass().getName()); |
| intent.putExtra(EXTRA_SESSION_ID, sessionId); |
| startActivity(intent); |
| return true; |
| }); |
| pref.setTitleContentDescription(AppUtils.getAppContentDescription(context, |
| packageName, user.getIdentifier())); |
| |
| updatePreferenceSummary(pref, summaryTimestamp); |
| |
| category.addPreference(pref); |
| if (!mViewModel.getCreationLogged()) { |
| logPermissionAppsFragmentCreated(packageName, user, viewIdForLogging, |
| grantCategory.equals(ALLOWED), grantCategory.equals(ALLOWED_FOREGROUND), |
| grantCategory.equals(DENIED)); |
| } |
| } |
| KotlinUtils.INSTANCE.sortPreferenceGroup(category, this::comparePreference, false); |
| } |
| |
| mViewModel.setCreationLogged(true); |
| |
| setLoading(false); |
| } |
| |
| @Override |
| public void onCreatePreferences(Bundle bundle, String s) { |
| setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getContext())); |
| } |
| |
| private void hideSystemAppToggleIfNecessary(Boolean hasSystemApps) { |
| if (hasSystemApps == null || !hasSystemApps) { |
| setAction(/* label= */ null, /* onClickListener= */ null); |
| } |
| } |
| |
| private void updatePreferenceSummary(Preference preference, |
| Pair<String, Integer> summaryTimestamp) { |
| String summary = mViewModel.getPreferenceSummary(getResources(), summaryTimestamp); |
| if (!summary.isEmpty()) { |
| preference.setSummary(summary); |
| } |
| } |
| |
| @Override |
| @RequiresApi(Build.VERSION_CODES.S) |
| public void onPermissionUsagesChanged() { |
| if (mPermissionUsages.getUsages().isEmpty()) { |
| return; |
| } |
| if (getContext() == null) { |
| // Async result has come in after our context is gone. |
| return; |
| } |
| |
| mAppPermissionUsages = new ArrayList<>(mPermissionUsages.getUsages()); |
| onPackagesLoaded(mViewModel.getCategorizedAppsLiveData().getValue()); |
| } |
| |
| private int comparePreference(Preference lhs, Preference rhs) { |
| return mViewModel.comparePreference(mCollator, lhs, rhs); |
| } |
| |
| private void logPermissionAppsFragmentCreated(String packageName, UserHandle user, long viewId, |
| boolean isAllowed, boolean isAllowedForeground, boolean isDenied) { |
| long sessionId = getArguments().getLong(EXTRA_SESSION_ID, 0); |
| mViewModel.logPermissionAppsFragmentCreated(packageName, user, viewId, isAllowed, |
| isAllowedForeground, isDenied, sessionId, getActivity().getApplication(), |
| mPermGroupName, LOG_TAG); |
| } |
| } |