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 = KotlinUtils.INSTANCE.is7DayToggleEnabled() 149 ? AppPermissionGroupsViewModel.AGGREGATE_DATA_FILTER_BEGIN_DAYS_7 : 150 AppPermissionGroupsViewModel.AGGREGATE_DATA_FILTER_BEGIN_DAYS_1; 151 152 long filterTimeBeginMillis = Math.max(System.currentTimeMillis() 153 - DAYS.toMillis(aggregateDataFilterBeginDays), 154 Instant.EPOCH.toEpochMilli()); 155 mPermissionUsages.load(null, null, filterTimeBeginMillis, Long.MAX_VALUE, 156 PermissionUsages.USAGE_FLAG_LAST, getActivity().getLoaderManager(), 157 false, false, this, false); 158 } 159 } 160 161 @Override onCreatePreferences(Bundle bundle, String s)162 public void onCreatePreferences(Bundle bundle, String s) { 163 setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getContext())); 164 } 165 166 167 @Override 168 @RequiresApi(Build.VERSION_CODES.S) onPermissionUsagesChanged()169 public void onPermissionUsagesChanged() { 170 if (mPermissionUsages.getUsages().isEmpty()) { 171 return; 172 } 173 if (getContext() == null) { 174 // Async result has come in after our context is gone. 175 return; 176 } 177 178 mAppPermissionUsages = new ArrayList<>(mPermissionUsages.getUsages()); 179 updatePreferences(mViewModel.getPackagePermGroupsLiveData().getValue()); 180 } 181 showAllPermissions()182 private void showAllPermissions() { 183 Fragment frag = AutoAllAppPermissionsFragment.newInstance( 184 getArguments().getString(Intent.EXTRA_PACKAGE_NAME), 185 getArguments().getParcelable(Intent.EXTRA_USER), 186 getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID)); 187 getFragmentManager().beginTransaction() 188 .replace(android.R.id.content, frag) 189 .addToBackStack("AllPerms") 190 .commit(); 191 } 192 bindUi(PackageInfo packageInfo)193 protected void bindUi(PackageInfo packageInfo) { 194 getPreferenceScreen().addPreference( 195 AutoPermissionsUtils.createHeaderPreference(getContext(), 196 packageInfo.applicationInfo)); 197 198 PreferenceGroup allowed = new PreferenceCategory(getContext()); 199 allowed.setKey(KEY_ALLOWED_PERMISSIONS_GROUP); 200 allowed.setTitle(R.string.allowed_header); 201 getPreferenceScreen().addPreference(allowed); 202 203 PreferenceGroup denied = new PreferenceCategory(getContext()); 204 denied.setKey(KEY_DENIED_PERMISSIONS_GROUP); 205 denied.setTitle(R.string.denied_header); 206 getPreferenceScreen().addPreference(denied); 207 } 208 createPreferenceCategories(PackageInfo packageInfo)209 private void createPreferenceCategories(PackageInfo packageInfo) { 210 bindUi(packageInfo); 211 } 212 updatePreferences(@ullable Map<Category, List<AppPermissionGroupsViewModel.GroupUiInfo>> groupMap)213 private void updatePreferences(@Nullable 214 Map<Category, List<AppPermissionGroupsViewModel.GroupUiInfo>> groupMap) { 215 if (groupMap == null && mViewModel.getPackagePermGroupsLiveData().isInitialized()) { 216 // null because explicitly set to null 217 Toast.makeText( 218 getActivity(), R.string.app_not_found_dlg_title, Toast.LENGTH_LONG).show(); 219 Log.w(LOG_TAG, "invalid package " + mPackageName); 220 221 getActivity().finish(); 222 return; 223 } else if (groupMap == null) { 224 // null because uninitialized 225 return; 226 } 227 228 Context context = getPreferenceManager().getContext(); 229 if (context == null) { 230 return; 231 } 232 233 Map<String, Long> groupUsageLastAccessTime = new HashMap<>(); 234 mViewModel.extractGroupUsageLastAccessTime(groupUsageLastAccessTime, mAppPermissionUsages, 235 mPackageName); 236 237 for (Category grantCategory : groupMap.keySet()) { 238 if (Category.ASK.equals(grantCategory)) { 239 // skip ask category for auto 240 continue; 241 } 242 PreferenceCategory category = getPreferenceScreen().findPreference( 243 grantCategory.getCategoryName()); 244 if (grantCategory.equals(Category.ALLOWED_FOREGROUND)) { 245 category = findPreference(Category.ALLOWED.getCategoryName()); 246 } 247 int numExtraPerms = 0; 248 249 category.removeAll(); 250 251 252 for (AppPermissionGroupsViewModel.GroupUiInfo groupInfo : groupMap.get(grantCategory)) { 253 if (groupInfo.isSystem() == mIsSystemPermsScreen) { 254 Preference preference = createPermissionPreference(getContext(), groupInfo, 255 groupUsageLastAccessTime); 256 category.addPreference(preference); 257 } else if (!groupInfo.isSystem()) { 258 numExtraPerms++; 259 } 260 } 261 262 263 if (numExtraPerms > 0) { 264 setAdditionalPermissionsPreference(category, numExtraPerms, context); 265 } 266 267 if (category.getPreferenceCount() == 0) { 268 setNoPermissionPreference(category, grantCategory, context); 269 } 270 271 KotlinUtils.INSTANCE.sortPreferenceGroup(category, this::comparePreferences, false); 272 } 273 274 275 if (mIsFirstLoad) { 276 logAppPermissionsFragmentView(); 277 mIsFirstLoad = false; 278 } 279 setLoading(false); 280 } 281 createPermissionPreference(Context context, AppPermissionGroupsViewModel.GroupUiInfo groupInfo, Map<String, Long> groupUsageLastAccessTime)282 private Preference createPermissionPreference(Context context, 283 AppPermissionGroupsViewModel.GroupUiInfo groupInfo, 284 Map<String, Long> groupUsageLastAccessTime) { 285 String groupName = groupInfo.getGroupName(); 286 Preference preference = new Preference(context); 287 preference.setTitle(KotlinUtils.INSTANCE.getPermGroupLabel(context, groupName)); 288 preference.setIcon(KotlinUtils.INSTANCE.getPermGroupIcon(context, groupName)); 289 preference.setKey(groupName); 290 String summary = mViewModel.getPreferenceSummary(groupInfo, context, 291 groupUsageLastAccessTime.get(groupName)); 292 if (!summary.isEmpty()) { 293 preference.setSummary(summary); 294 } 295 preference.setOnPreferenceClickListener(pref -> { 296 Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSION); 297 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mPackageName); 298 intent.putExtra(Intent.EXTRA_PERMISSION_NAME, groupName); 299 intent.putExtra(Intent.EXTRA_USER, mUser); 300 intent.putExtra(EXTRA_CALLER_NAME, AutoAppPermissionsFragment.class.getName()); 301 context.startActivity(intent); 302 return true; 303 }); 304 return preference; 305 } 306 setAdditionalPermissionsPreference(PreferenceCategory category, int numExtraPerms, Context context)307 private void setAdditionalPermissionsPreference(PreferenceCategory category, int numExtraPerms, 308 Context context) { 309 Preference extraPerms = new Preference(context); 310 extraPerms.setIcon(R.drawable.ic_toc); 311 extraPerms.setTitle(R.string.additional_permissions); 312 extraPerms.setOnPreferenceClickListener(preference -> { 313 AutoAppPermissionsFragment 314 frag = AutoAppPermissionsFragment.newInstance(mPackageName, mUser, 315 getArguments().getLong(EXTRA_SESSION_ID), false); 316 frag.setTargetFragment(AutoAppPermissionsFragment.this, 0); 317 getFragmentManager().beginTransaction() 318 .replace(android.R.id.content, frag) 319 .addToBackStack(null) 320 .commit(); 321 return true; 322 }); 323 extraPerms.setSummary(StringUtils.getIcuPluralsString(getContext(), 324 R.string.additional_permissions_more, numExtraPerms)); 325 category.addPreference(extraPerms); 326 } 327 setNoPermissionPreference(PreferenceCategory category, Category grantCategory, Context context)328 private void setNoPermissionPreference(PreferenceCategory category, Category grantCategory, 329 Context context) { 330 Preference empty = new Preference(context); 331 empty.setKey(getString(grantCategory.equals(Category.DENIED) 332 ? R.string.no_permissions_denied : R.string.no_permissions_allowed)); 333 empty.setTitle(empty.getKey()); 334 empty.setSelectable(false); 335 category.addPreference(empty); 336 } 337 comparePreferences(Preference lhs, Preference rhs)338 private int comparePreferences(Preference lhs, Preference rhs) { 339 String additionalTitle = lhs.getContext().getString(R.string.additional_permissions); 340 if (lhs.getTitle().equals(additionalTitle)) { 341 return 1; 342 } else if (rhs.getTitle().equals(additionalTitle)) { 343 return -1; 344 } 345 return mCollator.compare(lhs.getTitle().toString(), 346 rhs.getTitle().toString()); 347 } 348 logAppPermissionsFragmentView()349 private void logAppPermissionsFragmentView() { 350 Context context = getPreferenceManager().getContext(); 351 if (context == null) { 352 return; 353 } 354 String permissionSubtitleOnlyInForeground = 355 context.getString(R.string.permission_subtitle_only_in_foreground); 356 357 358 long sessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID); 359 long viewId = new Random().nextLong(); 360 361 PreferenceCategory allowed = findPreference(KEY_ALLOWED_PERMISSIONS_GROUP); 362 363 int numAllowed = allowed.getPreferenceCount(); 364 for (int i = 0; i < numAllowed; i++) { 365 Preference preference = allowed.getPreference(i); 366 if (preference.getTitle().equals(getString(R.string.no_permissions_allowed))) { 367 // R.string.no_permission_allowed was added to PreferenceCategory 368 continue; 369 } 370 371 int category = APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED; 372 if (preference.getSummary() != null 373 && permissionSubtitleOnlyInForeground.contentEquals(preference.getSummary())) { 374 category = APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND; 375 } 376 377 logAppPermissionsFragmentViewEntry(sessionId, viewId, preference.getKey(), category); 378 } 379 380 PreferenceCategory denied = findPreference(KEY_DENIED_PERMISSIONS_GROUP); 381 382 int numDenied = denied.getPreferenceCount(); 383 for (int i = 0; i < numDenied; i++) { 384 Preference preference = denied.getPreference(i); 385 if (preference.getTitle().equals(getString(R.string.no_permissions_denied))) { 386 // R.string.no_permission_denied was added to PreferenceCategory 387 continue; 388 } 389 logAppPermissionsFragmentViewEntry(sessionId, viewId, preference.getKey(), 390 APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__DENIED); 391 } 392 } 393 logAppPermissionsFragmentViewEntry( long sessionId, long viewId, String permissionGroupName, int category)394 private void logAppPermissionsFragmentViewEntry( 395 long sessionId, long viewId, String permissionGroupName, int category) { 396 Integer uid = KotlinUtils.INSTANCE.getPackageUid(getActivity().getApplication(), 397 mPackageName, mUser); 398 if (uid == null) { 399 return; 400 } 401 PermissionControllerStatsLog.write(APP_PERMISSIONS_FRAGMENT_VIEWED, sessionId, viewId, 402 permissionGroupName, uid, mPackageName, category); 403 Log.i(LOG_TAG, "AutoAppPermissionFragment view logged with sessionId=" + sessionId 404 + " viewId=" + viewId + " permissionGroupName=" + permissionGroupName + " uid=" 405 + uid + " packageName=" 406 + mPackageName + " category=" + category); 407 } 408 } 409 410