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