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.permissioncontroller.permission.ui.auto; 18 19 import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID; 20 import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID; 21 import static com.android.permissioncontroller.permission.ui.Category.ALLOWED; 22 import static com.android.permissioncontroller.permission.ui.Category.ALLOWED_FOREGROUND; 23 import static com.android.permissioncontroller.permission.ui.Category.ASK; 24 import static com.android.permissioncontroller.permission.ui.Category.DENIED; 25 import static com.android.permissioncontroller.permission.ui.Category.STORAGE_FOOTER; 26 import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_CALLER_NAME; 27 28 import android.content.Context; 29 import android.content.Intent; 30 import android.graphics.drawable.Drawable; 31 import android.os.Build; 32 import android.os.Bundle; 33 import android.os.UserHandle; 34 import android.util.ArrayMap; 35 36 import androidx.annotation.Nullable; 37 import androidx.annotation.RequiresApi; 38 import androidx.fragment.app.Fragment; 39 import androidx.lifecycle.ViewModelProvider; 40 import androidx.preference.Preference; 41 import androidx.preference.PreferenceCategory; 42 43 import com.android.modules.utils.build.SdkLevel; 44 import com.android.permissioncontroller.R; 45 import com.android.permissioncontroller.auto.AutoSettingsFrameFragment; 46 import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage; 47 import com.android.permissioncontroller.permission.model.v31.PermissionUsages; 48 import com.android.permissioncontroller.permission.ui.Category; 49 import com.android.permissioncontroller.permission.ui.handheld.SmartIconLoadPackagePermissionPreference; 50 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel; 51 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModelFactory; 52 import com.android.permissioncontroller.permission.utils.KotlinUtils; 53 import com.android.permissioncontroller.permission.utils.Utils; 54 import com.android.settingslib.utils.applications.AppUtils; 55 56 import kotlin.Pair; 57 import kotlin.Triple; 58 59 import java.text.Collator; 60 import java.util.ArrayList; 61 import java.util.List; 62 import java.util.Map; 63 import java.util.Random; 64 65 /** Shows the list of applications which have (or do not have) the given permission. */ 66 public class AutoPermissionAppsFragment extends AutoSettingsFrameFragment implements 67 PermissionUsages.PermissionsUsagesChangeCallback { 68 69 private static final String LOG_TAG = "AutoPermissionAppsFragment"; 70 private static final String KEY_EMPTY = "_empty"; 71 72 /** Creates a new instance of {@link AutoPermissionAppsFragment} for the given permission. */ newInstance(String permissionGroupName, long sessionId)73 public static AutoPermissionAppsFragment newInstance(String permissionGroupName, 74 long sessionId) { 75 return setPermissionGroupName(new AutoPermissionAppsFragment(), permissionGroupName, 76 sessionId); 77 } 78 setPermissionGroupName( T fragment, String permissionGroupName, long sessionId)79 private static <T extends Fragment> T setPermissionGroupName( 80 T fragment, String permissionGroupName, long sessionId) { 81 Bundle arguments = new Bundle(); 82 arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, permissionGroupName); 83 arguments.putLong(EXTRA_SESSION_ID, sessionId); 84 fragment.setArguments(arguments); 85 return fragment; 86 } 87 88 private PermissionAppsViewModel mViewModel; 89 private PermissionUsages mPermissionUsages; 90 private List<AppPermissionUsage> mAppPermissionUsages = new ArrayList<>(); 91 private String mPermGroupName; 92 93 private Collator mCollator; 94 95 @Override onCreate(@ullable Bundle savedInstanceState)96 public void onCreate(@Nullable Bundle savedInstanceState) { 97 super.onCreate(savedInstanceState); 98 setLoading(true); 99 100 mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME); 101 if (mPermGroupName == null) { 102 mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_NAME); 103 } 104 105 Drawable icon = KotlinUtils.INSTANCE.getPermGroupIcon(getContext(), mPermGroupName); 106 CharSequence label = KotlinUtils.INSTANCE.getPermGroupLabel(getContext(), mPermGroupName); 107 CharSequence description = KotlinUtils.INSTANCE.getPermGroupDescription(getContext(), 108 mPermGroupName); 109 110 setHeaderLabel(label); 111 Preference header = new Preference(getContext()); 112 header.setTitle(label); 113 header.setIcon(icon); 114 header.setSummary(Utils.getPermissionGroupDescriptionString(getContext(), mPermGroupName, 115 description)); 116 getPreferenceScreen().addPreference(header); 117 118 mCollator = Collator.getInstance( 119 getContext().getResources().getConfiguration().getLocales().get(0)); 120 121 PermissionAppsViewModelFactory factory = 122 new PermissionAppsViewModelFactory(getActivity().getApplication(), mPermGroupName, 123 this, new Bundle()); 124 mViewModel = new ViewModelProvider(this, factory).get(PermissionAppsViewModel.class); 125 126 mViewModel.getCategorizedAppsLiveData().observe(this, this::onPackagesLoaded); 127 mViewModel.getShouldShowSystemLiveData().observe(this, this::updateMenu); 128 mViewModel.getHasSystemAppsLiveData().observe(this, this::hideSystemAppToggleIfNecessary); 129 130 // If the build type is below S, the app ops for permission usage can't be found. Thus, we 131 // shouldn't load permission usages, for them. 132 if (SdkLevel.isAtLeastS()) { 133 Context context = getPreferenceManager().getContext(); 134 mPermissionUsages = new PermissionUsages(context); 135 136 long filterTimeBeginMillis = mViewModel.getFilterTimeBeginMillis(); 137 mPermissionUsages.load(null, null, filterTimeBeginMillis, Long.MAX_VALUE, 138 PermissionUsages.USAGE_FLAG_LAST, getActivity().getLoaderManager(), 139 false, false, this, false); 140 } 141 } 142 updateMenu(Boolean showSystem)143 private void updateMenu(Boolean showSystem) { 144 if (showSystem == null) { 145 showSystem = false; 146 } 147 // Show the opposite label from the current state. 148 String label; 149 if (showSystem) { 150 label = getString(R.string.menu_hide_system); 151 } else { 152 label = getString(R.string.menu_show_system); 153 } 154 155 boolean showSystemFinal = showSystem; 156 setAction(label, v -> mViewModel.updateShowSystem(!showSystemFinal)); 157 } 158 159 /** 160 * Main differences between this phone implementation and this one are: 161 * <ul> 162 * <li>No special handling for scoped storage</li> 163 * </ul> 164 */ onPackagesLoaded(Map<Category, List<Pair<String, UserHandle>>> categories)165 private void onPackagesLoaded(Map<Category, List<Pair<String, UserHandle>>> categories) { 166 Preference additionalPermissionsPreference = getPreferenceScreen().findPreference( 167 ALLOWED_FOREGROUND.getCategoryName()); 168 if (additionalPermissionsPreference == null) { 169 // This preference resources includes the "Ask" permission group. That's okay for Auto 170 // even though Auto doesn't support the one-time permission because the code later in 171 // this method will hide unused permission groups. 172 addPreferencesFromResource(R.xml.allowed_denied); 173 } 174 // Hide allowed foreground label by default, to avoid briefly showing it before updating 175 findPreference(ALLOWED_FOREGROUND.getCategoryName()).setVisible(false); 176 177 // Hide storage footer category 178 findPreference(STORAGE_FOOTER.getCategoryName()).setVisible(false); 179 180 Context context = getPreferenceManager().getContext(); 181 182 if (context == null || getActivity() == null || categories == null) { 183 return; 184 } 185 186 Map<String, Preference> existingPrefs = new ArrayMap<>(); 187 188 // Start at 1 since the header preference will always be in the 0th index 189 for (int i = 1; i < getPreferenceScreen().getPreferenceCount(); i++) { 190 PreferenceCategory category = (PreferenceCategory) 191 getPreferenceScreen().getPreference(i); 192 category.setOrderingAsAdded(true); 193 int numPreferences = category.getPreferenceCount(); 194 for (int j = 0; j < numPreferences; j++) { 195 Preference preference = category.getPreference(j); 196 existingPrefs.put(preference.getKey(), preference); 197 } 198 category.removeAll(); 199 } 200 201 long viewIdForLogging = new Random().nextLong(); 202 long sessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID); 203 204 Boolean showAlways = mViewModel.getShowAllowAlwaysStringLiveData().getValue(); 205 if (showAlways != null && showAlways) { 206 findPreference(ALLOWED.getCategoryName()).setTitle(R.string.allowed_always_header); 207 } else { 208 findPreference(ALLOWED.getCategoryName()).setTitle(R.string.allowed_header); 209 } 210 211 // A mapping of user + packageName to their last access timestamps for the permission group. 212 Map<String, Long> groupUsageLastAccessTime = 213 mViewModel.extractGroupUsageLastAccessTime(mAppPermissionUsages); 214 215 for (Category grantCategory : categories.keySet()) { 216 List<Pair<String, UserHandle>> packages = categories.get(grantCategory); 217 PreferenceCategory category = findPreference(grantCategory.getCategoryName()); 218 219 // If this category is empty set up the empty preference. 220 if (packages.size() == 0) { 221 Preference empty = new Preference(context); 222 empty.setSelectable(false); 223 empty.setKey(category.getKey() + KEY_EMPTY); 224 if (grantCategory.equals(ALLOWED)) { 225 empty.setTitle(getString(R.string.no_apps_allowed)); 226 } else if (grantCategory.equals(ALLOWED_FOREGROUND)) { 227 category.setVisible(false); 228 } else if (grantCategory.equals(ASK)) { 229 category.setVisible(false); 230 } else { 231 empty.setTitle(getString(R.string.no_apps_denied)); 232 } 233 category.addPreference(empty); 234 continue; 235 } else if (grantCategory.equals(ALLOWED_FOREGROUND)) { 236 category.setVisible(true); 237 } else if (grantCategory.equals(ASK)) { 238 category.setVisible(true); 239 } 240 241 for (Pair<String, UserHandle> packageUserLabel : packages) { 242 String packageName = packageUserLabel.getFirst(); 243 UserHandle user = packageUserLabel.getSecond(); 244 245 String key = user + packageName; 246 247 Long lastAccessTime = groupUsageLastAccessTime.get(key); 248 Triple<String, Integer, String> summaryTimestamp = Utils 249 .getPermissionLastAccessSummaryTimestamp( 250 lastAccessTime, context, mPermGroupName); 251 252 Preference existingPref = existingPrefs.get(key); 253 if (existingPref != null) { 254 updatePreferenceSummary(existingPref, summaryTimestamp); 255 category.addPreference(existingPref); 256 continue; 257 } 258 259 SmartIconLoadPackagePermissionPreference pref = 260 new SmartIconLoadPackagePermissionPreference(getActivity().getApplication(), 261 packageName, user, context); 262 pref.setKey(key); 263 pref.setTitle(KotlinUtils.INSTANCE.getPackageLabel(getActivity().getApplication(), 264 packageName, user)); 265 pref.setOnPreferenceClickListener((Preference p) -> { 266 Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSION); 267 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); 268 intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, mPermGroupName); 269 intent.putExtra(Intent.EXTRA_USER, user); 270 intent.putExtra(EXTRA_CALLER_NAME, getClass().getName()); 271 intent.putExtra(EXTRA_SESSION_ID, sessionId); 272 startActivity(intent); 273 return true; 274 }); 275 pref.setTitleContentDescription(AppUtils.getAppContentDescription(context, 276 packageName, user.getIdentifier())); 277 278 updatePreferenceSummary(pref, summaryTimestamp); 279 280 category.addPreference(pref); 281 if (!mViewModel.getCreationLogged()) { 282 logPermissionAppsFragmentCreated(packageName, user, viewIdForLogging, 283 grantCategory.equals(ALLOWED), grantCategory.equals(ALLOWED_FOREGROUND), 284 grantCategory.equals(DENIED)); 285 } 286 } 287 KotlinUtils.INSTANCE.sortPreferenceGroup(category, this::comparePreference, false); 288 } 289 290 mViewModel.setCreationLogged(true); 291 292 setLoading(false); 293 } 294 295 @Override onCreatePreferences(Bundle bundle, String s)296 public void onCreatePreferences(Bundle bundle, String s) { 297 setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getContext())); 298 } 299 hideSystemAppToggleIfNecessary(Boolean hasSystemApps)300 private void hideSystemAppToggleIfNecessary(Boolean hasSystemApps) { 301 if (hasSystemApps == null || !hasSystemApps) { 302 setAction(/* label= */ null, /* onClickListener= */ null); 303 } 304 } 305 updatePreferenceSummary(Preference preference, Triple<String, Integer, String> summaryTimestamp)306 private void updatePreferenceSummary(Preference preference, 307 Triple<String, Integer, String> summaryTimestamp) { 308 String summary = mViewModel.getPreferenceSummary(getResources(), summaryTimestamp); 309 if (!summary.isEmpty()) { 310 preference.setSummary(summary); 311 } 312 } 313 314 @Override 315 @RequiresApi(Build.VERSION_CODES.S) onPermissionUsagesChanged()316 public void onPermissionUsagesChanged() { 317 if (mPermissionUsages.getUsages().isEmpty()) { 318 return; 319 } 320 if (getContext() == null) { 321 // Async result has come in after our context is gone. 322 return; 323 } 324 325 mAppPermissionUsages = new ArrayList<>(mPermissionUsages.getUsages()); 326 onPackagesLoaded(mViewModel.getCategorizedAppsLiveData().getValue()); 327 } 328 comparePreference(Preference lhs, Preference rhs)329 private int comparePreference(Preference lhs, Preference rhs) { 330 return mViewModel.comparePreference(mCollator, lhs, rhs); 331 } 332 logPermissionAppsFragmentCreated(String packageName, UserHandle user, long viewId, boolean isAllowed, boolean isAllowedForeground, boolean isDenied)333 private void logPermissionAppsFragmentCreated(String packageName, UserHandle user, long viewId, 334 boolean isAllowed, boolean isAllowedForeground, boolean isDenied) { 335 long sessionId = getArguments().getLong(EXTRA_SESSION_ID, 0); 336 mViewModel.logPermissionAppsFragmentCreated(packageName, user, viewId, isAllowed, 337 isAllowedForeground, isDenied, sessionId, getActivity().getApplication(), 338 mPermGroupName, LOG_TAG); 339 } 340 } 341