1 /* 2 * Copyright 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.settings.applications.specialaccess; 18 19 import android.app.AppOpsManager; 20 import android.car.drivingstate.CarUxRestrictions; 21 import android.content.Context; 22 import android.content.pm.ApplicationInfo; 23 import android.content.pm.PackageManager; 24 25 import androidx.annotation.CallSuper; 26 import androidx.annotation.VisibleForTesting; 27 import androidx.preference.Preference; 28 import androidx.preference.PreferenceGroup; 29 30 import com.android.car.settings.R; 31 import com.android.car.settings.applications.specialaccess.AppStateAppOpsBridge.PermissionState; 32 import com.android.car.settings.common.FragmentController; 33 import com.android.car.settings.common.PreferenceController; 34 import com.android.car.ui.preference.CarUiSwitchPreference; 35 import com.android.settingslib.applications.ApplicationsState; 36 import com.android.settingslib.applications.ApplicationsState.AppEntry; 37 import com.android.settingslib.applications.ApplicationsState.AppFilter; 38 import com.android.settingslib.applications.ApplicationsState.CompoundFilter; 39 40 import java.util.List; 41 42 /** 43 * Displays a list of toggles for applications requesting permission to perform the operation with 44 * which this controller was initialized. {@link #init(int, String, int)} should be called when 45 * this controller is instantiated to specify the {@link AppOpsManager} operation code to control 46 * access for. 47 */ 48 public class AppOpsPreferenceController extends PreferenceController<PreferenceGroup> { 49 50 private static final AppFilter FILTER_HAS_INFO = new AppFilter() { 51 @Override 52 public void init() { 53 // No op. 54 } 55 56 @Override 57 public boolean filterApp(AppEntry info) { 58 return info.extraInfo != null; 59 } 60 }; 61 62 private final AppOpsManager mAppOpsManager; 63 64 private final Preference.OnPreferenceChangeListener mOnPreferenceChangeListener = 65 new Preference.OnPreferenceChangeListener() { 66 @Override 67 public boolean onPreferenceChange(Preference preference, Object newValue) { 68 AppOpPreference appOpPreference = (AppOpPreference) preference; 69 AppEntry entry = appOpPreference.mEntry; 70 PermissionState extraInfo = (PermissionState) entry.extraInfo; 71 boolean allowOp = (Boolean) newValue; 72 if (allowOp != extraInfo.isPermissible()) { 73 mAppOpsManager.setMode(mAppOpsOpCode, entry.info.uid, 74 entry.info.packageName, 75 allowOp ? AppOpsManager.MODE_ALLOWED : mNegativeOpMode); 76 // Update the extra info of this entry so that it reflects the new mode. 77 mAppEntryListManager.forceUpdate(entry); 78 return true; 79 } 80 return false; 81 } 82 }; 83 84 private final AppEntryListManager.Callback mCallback = new AppEntryListManager.Callback() { 85 @Override 86 public void onAppEntryListChanged(List<AppEntry> entries) { 87 mEntries = entries; 88 refreshUi(); 89 } 90 }; 91 92 private int mAppOpsOpCode = AppOpsManager.OP_NONE; 93 private String mPermission; 94 private int mNegativeOpMode = -1; 95 96 @VisibleForTesting 97 AppEntryListManager mAppEntryListManager; 98 private List<AppEntry> mEntries; 99 100 private boolean mShowSystem; 101 AppOpsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)102 public AppOpsPreferenceController(Context context, String preferenceKey, 103 FragmentController fragmentController, CarUxRestrictions uxRestrictions) { 104 this(context, preferenceKey, fragmentController, uxRestrictions, 105 context.getSystemService(AppOpsManager.class)); 106 } 107 108 @VisibleForTesting AppOpsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions, AppOpsManager appOpsManager)109 AppOpsPreferenceController(Context context, String preferenceKey, 110 FragmentController fragmentController, CarUxRestrictions uxRestrictions, 111 AppOpsManager appOpsManager) { 112 super(context, preferenceKey, fragmentController, uxRestrictions); 113 mAppOpsManager = appOpsManager; 114 mAppEntryListManager = new AppEntryListManager(context); 115 } 116 117 @Override getPreferenceType()118 protected Class<PreferenceGroup> getPreferenceType() { 119 return PreferenceGroup.class; 120 } 121 122 /** 123 * Initializes this controller with the {@code appOpsOpCode} (selected from the operations in 124 * {@link AppOpsManager}) to control access for. 125 * 126 * @param permission the {@link android.Manifest.permission} apps must hold to perform the 127 * operation. 128 * @param negativeOpMode the operation mode that will be passed to {@link 129 * AppOpsManager#setMode(int, int, String, int)} when access for a app is 130 * revoked. 131 */ init(int appOpsOpCode, String permission, int negativeOpMode)132 public void init(int appOpsOpCode, String permission, int negativeOpMode) { 133 mAppOpsOpCode = appOpsOpCode; 134 mPermission = permission; 135 mNegativeOpMode = negativeOpMode; 136 } 137 138 /** 139 * Rebuilds the preference list to show system applications if {@code showSystem} is true. 140 * System applications will be hidden otherwise. 141 */ setShowSystem(boolean showSystem)142 public void setShowSystem(boolean showSystem) { 143 if (mShowSystem != showSystem) { 144 mShowSystem = showSystem; 145 mAppEntryListManager.forceUpdate(); 146 } 147 } 148 149 @Override checkInitialized()150 protected void checkInitialized() { 151 if (mAppOpsOpCode == AppOpsManager.OP_NONE) { 152 throw new IllegalStateException("App operation code must be initialized"); 153 } 154 if (mPermission == null) { 155 throw new IllegalStateException("Manifest permission must be initialized"); 156 } 157 if (mNegativeOpMode == -1) { 158 throw new IllegalStateException("Negative case app operation mode must be initialized"); 159 } 160 } 161 162 @Override onCreateInternal()163 protected void onCreateInternal() { 164 AppStateAppOpsBridge extraInfoBridge = new AppStateAppOpsBridge(getContext(), mAppOpsOpCode, 165 mPermission); 166 mAppEntryListManager.init(extraInfoBridge, this::getAppFilter, mCallback); 167 } 168 169 @Override onStartInternal()170 protected void onStartInternal() { 171 mAppEntryListManager.start(); 172 } 173 174 @Override onStopInternal()175 protected void onStopInternal() { 176 mAppEntryListManager.stop(); 177 } 178 179 @Override onDestroyInternal()180 protected void onDestroyInternal() { 181 mAppEntryListManager.destroy(); 182 } 183 184 @Override updateState(PreferenceGroup preference)185 protected void updateState(PreferenceGroup preference) { 186 if (mEntries == null) { 187 // Still loading. 188 return; 189 } 190 preference.removeAll(); 191 for (AppEntry entry : mEntries) { 192 Preference appOpPreference = new AppOpPreference(getContext(), entry); 193 appOpPreference.setOnPreferenceChangeListener(mOnPreferenceChangeListener); 194 preference.addPreference(appOpPreference); 195 } 196 } 197 198 @CallSuper getAppFilter()199 protected AppFilter getAppFilter() { 200 AppFilter filterObj = new CompoundFilter(FILTER_HAS_INFO, 201 ApplicationsState.FILTER_NOT_HIDE); 202 if (!mShowSystem) { 203 filterObj = new CompoundFilter(filterObj, 204 ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER); 205 } 206 return filterObj; 207 } 208 209 private static class AppOpPreference extends CarUiSwitchPreference { 210 211 private final AppEntry mEntry; 212 AppOpPreference(Context context, AppEntry entry)213 AppOpPreference(Context context, AppEntry entry) { 214 super(context); 215 String key = entry.info.packageName + "|" + entry.info.uid; 216 setKey(key); 217 setTitle(entry.label); 218 setIcon(entry.icon); 219 setSummary(getAppStateText(entry.info)); 220 setPersistent(false); 221 PermissionState extraInfo = (PermissionState) entry.extraInfo; 222 setChecked(extraInfo.isPermissible()); 223 mEntry = entry; 224 } 225 getAppStateText(ApplicationInfo info)226 private String getAppStateText(ApplicationInfo info) { 227 if ((info.flags & ApplicationInfo.FLAG_INSTALLED) == 0) { 228 return getContext().getString(R.string.not_installed); 229 } else if (!info.enabled || info.enabledSetting 230 == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) { 231 return getContext().getString(R.string.disabled); 232 } 233 return null; 234 } 235 } 236 } 237