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.PermissionControllerStatsLog.APP_PERMISSIONS_FRAGMENT_VIEWED; 22 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED; 23 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND; 24 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__DENIED; 25 import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_CALLER_NAME; 26 27 import static java.util.concurrent.TimeUnit.DAYS; 28 29 import android.app.Activity; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.pm.PackageInfo; 33 import android.os.Build; 34 import android.os.Bundle; 35 import android.os.UserHandle; 36 import android.util.Log; 37 import android.widget.Toast; 38 39 import androidx.annotation.NonNull; 40 import androidx.annotation.Nullable; 41 import androidx.annotation.RequiresApi; 42 import androidx.fragment.app.Fragment; 43 import androidx.lifecycle.ViewModelProvider; 44 import androidx.preference.Preference; 45 import androidx.preference.PreferenceCategory; 46 import androidx.preference.PreferenceGroup; 47 48 import com.android.modules.utils.build.SdkLevel; 49 import com.android.permissioncontroller.PermissionControllerStatsLog; 50 import com.android.permissioncontroller.R; 51 import com.android.permissioncontroller.auto.AutoSettingsFrameFragment; 52 import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage; 53 import com.android.permissioncontroller.permission.model.v31.PermissionUsages; 54 import com.android.permissioncontroller.permission.ui.Category; 55 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModel; 56 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModelFactory; 57 import com.android.permissioncontroller.permission.utils.KotlinUtils; 58 import com.android.permissioncontroller.permission.utils.StringUtils; 59 60 import java.text.Collator; 61 import java.time.Instant; 62 import java.util.ArrayList; 63 import java.util.HashMap; 64 import java.util.List; 65 import java.util.Map; 66 import java.util.Random; 67 68 /** Screen to show the permissions for a specific application. */ 69 public class AutoAppPermissionsFragment extends AutoSettingsFrameFragment implements 70 PermissionUsages.PermissionsUsagesChangeCallback { 71 private static final String LOG_TAG = AutoAppPermissionsFragment.class.getSimpleName(); 72 73 private static final String IS_SYSTEM_PERMS_SCREEN = "_is_system_screen"; 74 private static final String KEY_ALLOWED_PERMISSIONS_GROUP = Category.ALLOWED.getCategoryName(); 75 private static final String KEY_DENIED_PERMISSIONS_GROUP = Category.DENIED.getCategoryName(); 76 77 private AppPermissionGroupsViewModel mViewModel; 78 79 private String mPackageName; 80 private boolean mIsFirstLoad; 81 private UserHandle mUser; 82 private PermissionUsages mPermissionUsages; 83 private List<AppPermissionUsage> mAppPermissionUsages = new ArrayList<>(); 84 private boolean mIsSystemPermsScreen; 85 86 private Collator mCollator; 87 88 /** 89 * @return A new fragment 90 */ newInstance(@onNull String packageName, @NonNull UserHandle userHandle, long sessionId, boolean isSystemPermsScreen)91 public static AutoAppPermissionsFragment newInstance(@NonNull String packageName, 92 @NonNull UserHandle userHandle, long sessionId, boolean isSystemPermsScreen) { 93 return setPackageNameAndUserHandle(new AutoAppPermissionsFragment(), packageName, 94 userHandle, sessionId, isSystemPermsScreen); 95 } 96 setPackageNameAndUserHandle(@onNull T fragment, @NonNull String packageName, @NonNull UserHandle userHandle, long sessionId, boolean isSystemPermsScreen)97 private static <T extends Fragment> T setPackageNameAndUserHandle(@NonNull T fragment, 98 @NonNull String packageName, @NonNull UserHandle userHandle, long sessionId, 99 boolean isSystemPermsScreen) { 100 Bundle arguments = new Bundle(); 101 arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName); 102 arguments.putParcelable(Intent.EXTRA_USER, userHandle); 103 arguments.putLong(EXTRA_SESSION_ID, sessionId); 104 arguments.putBoolean(IS_SYSTEM_PERMS_SCREEN, isSystemPermsScreen); 105 fragment.setArguments(arguments); 106 return fragment; 107 } 108 109 @Override onCreate(Bundle savedInstanceState)110 public void onCreate(Bundle savedInstanceState) { 111 super.onCreate(savedInstanceState); 112 setLoading(true); 113 114 mIsFirstLoad = true; 115 mPackageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); 116 mUser = getArguments().getParcelable(Intent.EXTRA_USER); 117 mIsSystemPermsScreen = getArguments().getBoolean(IS_SYSTEM_PERMS_SCREEN, true); 118 UserHandle userHandle = getArguments().getParcelable(Intent.EXTRA_USER); 119 Activity activity = requireActivity(); 120 PackageInfo packageInfo = AutoPermissionsUtils.getPackageInfo(activity, mPackageName, 121 userHandle); 122 if (packageInfo == null) { 123 Toast.makeText(getContext(), R.string.app_not_found_dlg_title, 124 Toast.LENGTH_LONG).show(); 125 activity.finish(); 126 return; 127 } 128 129 mCollator = Collator.getInstance( 130 getContext().getResources().getConfiguration().getLocales().get(0)); 131 AppPermissionGroupsViewModelFactory factory = 132 new AppPermissionGroupsViewModelFactory(mPackageName, userHandle, 133 getArguments().getLong(EXTRA_SESSION_ID, 0)); 134 mViewModel = new ViewModelProvider(this, factory).get(AppPermissionGroupsViewModel.class); 135 136 setHeaderLabel(getContext().getString(R.string.app_permissions)); 137 if (mIsSystemPermsScreen) { 138 setAction(getContext().getString(R.string.all_permissions), v -> showAllPermissions()); 139 } 140 createPreferenceCategories(packageInfo); 141 142 mViewModel.getPackagePermGroupsLiveData().observe(this, this::updatePreferences); 143 updatePreferences(mViewModel.getPackagePermGroupsLiveData().getValue()); 144 145 if (SdkLevel.isAtLeastS()) { 146 mPermissionUsages = new PermissionUsages(getContext()); 147 148 long aggregateDataFilterBeginDays = 149 AppPermissionGroupsViewModel.AGGREGATE_DATA_FILTER_BEGIN_DAYS_1; 150 151 long filterTimeBeginMillis = Math.max(System.currentTimeMillis() 152 - DAYS.toMillis(aggregateDataFilterBeginDays), 153 Instant.EPOCH.toEpochMilli()); 154 mPermissionUsages.load(null, null, filterTimeBeginMillis, Long.MAX_VALUE, 155 PermissionUsages.USAGE_FLAG_LAST, getActivity().getLoaderManager(), 156 false, false, this, false); 157 } 158 } 159 160 @Override onCreatePreferences(Bundle bundle, String s)161 public void onCreatePreferences(Bundle bundle, String s) { 162 setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getContext())); 163 } 164 165 166 @Override 167 @RequiresApi(Build.VERSION_CODES.S) onPermissionUsagesChanged()168 public void onPermissionUsagesChanged() { 169 if (mPermissionUsages.getUsages().isEmpty()) { 170 return; 171 } 172 if (getContext() == null) { 173 // Async result has come in after our context is gone. 174 return; 175 } 176 177 mAppPermissionUsages = new ArrayList<>(mPermissionUsages.getUsages()); 178 updatePreferences(mViewModel.getPackagePermGroupsLiveData().getValue()); 179 } 180 showAllPermissions()181 private void showAllPermissions() { 182 Fragment frag = AutoAllAppPermissionsFragment.newInstance( 183 getArguments().getString(Intent.EXTRA_PACKAGE_NAME), 184 getArguments().getParcelable(Intent.EXTRA_USER), 185 getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID)); 186 getFragmentManager().beginTransaction() 187 .replace(android.R.id.content, frag) 188 .addToBackStack("AllPerms") 189 .commit(); 190 } 191 bindUi(PackageInfo packageInfo)192 protected void bindUi(PackageInfo packageInfo) { 193 getPreferenceScreen().addPreference( 194 AutoPermissionsUtils.createHeaderPreference(getContext(), 195 packageInfo.applicationInfo)); 196 197 PreferenceGroup allowed = new PreferenceCategory(getContext()); 198 allowed.setKey(KEY_ALLOWED_PERMISSIONS_GROUP); 199 allowed.setTitle(R.string.allowed_header); 200 getPreferenceScreen().addPreference(allowed); 201 202 PreferenceGroup denied = new PreferenceCategory(getContext()); 203 denied.setKey(KEY_DENIED_PERMISSIONS_GROUP); 204 denied.setTitle(R.string.denied_header); 205 getPreferenceScreen().addPreference(denied); 206 } 207 createPreferenceCategories(PackageInfo packageInfo)208 private void createPreferenceCategories(PackageInfo packageInfo) { 209 bindUi(packageInfo); 210 } 211 updatePreferences(@ullable Map<Category, List<AppPermissionGroupsViewModel.GroupUiInfo>> groupMap)212 private void updatePreferences(@Nullable 213 Map<Category, List<AppPermissionGroupsViewModel.GroupUiInfo>> groupMap) { 214 if (groupMap == null && mViewModel.getPackagePermGroupsLiveData().isInitialized()) { 215 // null because explicitly set to null 216 Toast.makeText( 217 getActivity(), R.string.app_not_found_dlg_title, Toast.LENGTH_LONG).show(); 218 Log.w(LOG_TAG, "invalid package " + mPackageName); 219 220 getActivity().finish(); 221 return; 222 } else if (groupMap == null) { 223 // null because uninitialized 224 return; 225 } 226 227 Context context = getPreferenceManager().getContext(); 228 if (context == null) { 229 return; 230 } 231 232 Map<String, Long> groupUsageLastAccessTime = new HashMap<>(); 233 mViewModel.extractGroupUsageLastAccessTime(groupUsageLastAccessTime, mAppPermissionUsages, 234 mPackageName); 235 236 for (Category grantCategory : groupMap.keySet()) { 237 if (Category.ASK.equals(grantCategory)) { 238 // skip ask category for auto 239 continue; 240 } 241 PreferenceCategory category = getPreferenceScreen().findPreference( 242 grantCategory.getCategoryName()); 243 if (grantCategory.equals(Category.ALLOWED_FOREGROUND)) { 244 category = findPreference(Category.ALLOWED.getCategoryName()); 245 } 246 int numExtraPerms = 0; 247 248 category.removeAll(); 249 250 251 for (AppPermissionGroupsViewModel.GroupUiInfo groupInfo : groupMap.get(grantCategory)) { 252 if (groupInfo.isSystem() == mIsSystemPermsScreen) { 253 Preference preference = createPermissionPreference(getContext(), groupInfo, 254 groupUsageLastAccessTime); 255 category.addPreference(preference); 256 } else if (!groupInfo.isSystem()) { 257 numExtraPerms++; 258 } 259 } 260 261 262 if (numExtraPerms > 0) { 263 setAdditionalPermissionsPreference(category, numExtraPerms, context); 264 } 265 266 if (category.getPreferenceCount() == 0) { 267 setNoPermissionPreference(category, grantCategory, context); 268 } 269 270 KotlinUtils.INSTANCE.sortPreferenceGroup(category, this::comparePreferences, false); 271 } 272 273 274 if (mIsFirstLoad) { 275 logAppPermissionsFragmentView(); 276 mIsFirstLoad = false; 277 } 278 setLoading(false); 279 } 280 createPermissionPreference(Context context, AppPermissionGroupsViewModel.GroupUiInfo groupInfo, Map<String, Long> groupUsageLastAccessTime)281 private Preference createPermissionPreference(Context context, 282 AppPermissionGroupsViewModel.GroupUiInfo groupInfo, 283 Map<String, Long> groupUsageLastAccessTime) { 284 String groupName = groupInfo.getGroupName(); 285 Preference preference = new Preference(context); 286 preference.setTitle(KotlinUtils.INSTANCE.getPermGroupLabel(context, groupName)); 287 preference.setIcon(KotlinUtils.INSTANCE.getPermGroupIcon(context, groupName)); 288 preference.setKey(groupName); 289 String summary = mViewModel.getPreferenceSummary(groupInfo, context, 290 groupUsageLastAccessTime.get(groupName)); 291 if (!summary.isEmpty()) { 292 preference.setSummary(summary); 293 } 294 preference.setOnPreferenceClickListener(pref -> { 295 Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSION); 296 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mPackageName); 297 intent.putExtra(Intent.EXTRA_PERMISSION_NAME, groupName); 298 intent.putExtra(Intent.EXTRA_USER, mUser); 299 intent.putExtra(EXTRA_CALLER_NAME, AutoAppPermissionsFragment.class.getName()); 300 context.startActivity(intent); 301 return true; 302 }); 303 return preference; 304 } 305 setAdditionalPermissionsPreference(PreferenceCategory category, int numExtraPerms, Context context)306 private void setAdditionalPermissionsPreference(PreferenceCategory category, int numExtraPerms, 307 Context context) { 308 Preference extraPerms = new Preference(context); 309 extraPerms.setIcon(R.drawable.ic_toc); 310 extraPerms.setTitle(R.string.additional_permissions); 311 extraPerms.setOnPreferenceClickListener(preference -> { 312 AutoAppPermissionsFragment 313 frag = AutoAppPermissionsFragment.newInstance(mPackageName, mUser, 314 getArguments().getLong(EXTRA_SESSION_ID), false); 315 frag.setTargetFragment(AutoAppPermissionsFragment.this, 0); 316 getFragmentManager().beginTransaction() 317 .replace(android.R.id.content, frag) 318 .addToBackStack(null) 319 .commit(); 320 return true; 321 }); 322 extraPerms.setSummary(StringUtils.getIcuPluralsString(getContext(), 323 R.string.additional_permissions_more, numExtraPerms)); 324 category.addPreference(extraPerms); 325 } 326 setNoPermissionPreference(PreferenceCategory category, Category grantCategory, Context context)327 private void setNoPermissionPreference(PreferenceCategory category, Category grantCategory, 328 Context context) { 329 Preference empty = new Preference(context); 330 empty.setKey(getString(grantCategory.equals(Category.DENIED) 331 ? R.string.no_permissions_denied : R.string.no_permissions_allowed)); 332 empty.setTitle(empty.getKey()); 333 empty.setSelectable(false); 334 category.addPreference(empty); 335 } 336 comparePreferences(Preference lhs, Preference rhs)337 private int comparePreferences(Preference lhs, Preference rhs) { 338 String additionalTitle = lhs.getContext().getString(R.string.additional_permissions); 339 if (lhs.getTitle().equals(additionalTitle)) { 340 return 1; 341 } else if (rhs.getTitle().equals(additionalTitle)) { 342 return -1; 343 } 344 return mCollator.compare(lhs.getTitle().toString(), 345 rhs.getTitle().toString()); 346 } 347 logAppPermissionsFragmentView()348 private void logAppPermissionsFragmentView() { 349 Context context = getPreferenceManager().getContext(); 350 if (context == null) { 351 return; 352 } 353 String permissionSubtitleOnlyInForeground = 354 context.getString(R.string.permission_subtitle_only_in_foreground); 355 356 357 long sessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID); 358 long viewId = new Random().nextLong(); 359 360 PreferenceCategory allowed = findPreference(KEY_ALLOWED_PERMISSIONS_GROUP); 361 362 int numAllowed = allowed.getPreferenceCount(); 363 for (int i = 0; i < numAllowed; i++) { 364 Preference preference = allowed.getPreference(i); 365 if (preference.getTitle().equals(getString(R.string.no_permissions_allowed))) { 366 // R.string.no_permission_allowed was added to PreferenceCategory 367 continue; 368 } 369 370 int category = APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED; 371 if (preference.getSummary() != null 372 && permissionSubtitleOnlyInForeground.contentEquals(preference.getSummary())) { 373 category = APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND; 374 } 375 376 logAppPermissionsFragmentViewEntry(sessionId, viewId, preference.getKey(), category); 377 } 378 379 PreferenceCategory denied = findPreference(KEY_DENIED_PERMISSIONS_GROUP); 380 381 int numDenied = denied.getPreferenceCount(); 382 for (int i = 0; i < numDenied; i++) { 383 Preference preference = denied.getPreference(i); 384 if (preference.getTitle().equals(getString(R.string.no_permissions_denied))) { 385 // R.string.no_permission_denied was added to PreferenceCategory 386 continue; 387 } 388 logAppPermissionsFragmentViewEntry(sessionId, viewId, preference.getKey(), 389 APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__DENIED); 390 } 391 } 392 logAppPermissionsFragmentViewEntry( long sessionId, long viewId, String permissionGroupName, int category)393 private void logAppPermissionsFragmentViewEntry( 394 long sessionId, long viewId, String permissionGroupName, int category) { 395 Integer uid = KotlinUtils.INSTANCE.getPackageUid(getActivity().getApplication(), 396 mPackageName, mUser); 397 if (uid == null) { 398 return; 399 } 400 PermissionControllerStatsLog.write(APP_PERMISSIONS_FRAGMENT_VIEWED, sessionId, viewId, 401 permissionGroupName, uid, mPackageName, category); 402 Log.i(LOG_TAG, "AutoAppPermissionFragment view logged with sessionId=" + sessionId 403 + " viewId=" + viewId + " permissionGroupName=" + permissionGroupName + " uid=" 404 + uid + " packageName=" 405 + mPackageName + " category=" + category); 406 } 407 } 408 409