1 /* 2 * Copyright (C) 2018 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.role.ui; 18 19 import static com.android.permissioncontroller.PermissionControllerStatsLog.ROLE_SETTINGS_FRAGMENT_ACTION_REPORTED; 20 21 import android.app.Activity; 22 import android.app.role.RoleManager; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.ApplicationInfo; 26 import android.graphics.drawable.Drawable; 27 import android.os.Bundle; 28 import android.os.Process; 29 import android.os.UserHandle; 30 import android.util.ArrayMap; 31 32 import androidx.annotation.NonNull; 33 import androidx.annotation.Nullable; 34 import androidx.appcompat.content.res.AppCompatResources; 35 import androidx.fragment.app.Fragment; 36 import androidx.lifecycle.ViewModelProvider; 37 import androidx.preference.Preference; 38 import androidx.preference.PreferenceCategory; 39 import androidx.preference.PreferenceFragmentCompat; 40 import androidx.preference.PreferenceGroup; 41 import androidx.preference.PreferenceManager; 42 import androidx.preference.PreferenceScreen; 43 import androidx.preference.TwoStatePreference; 44 45 import com.android.modules.utils.build.SdkLevel; 46 import com.android.permission.flags.Flags; 47 import com.android.permissioncontroller.PermissionControllerStatsLog; 48 import com.android.permissioncontroller.R; 49 import com.android.permissioncontroller.permission.utils.Utils; 50 import com.android.permissioncontroller.role.utils.PackageUtils; 51 import com.android.permissioncontroller.role.utils.RoleUiBehaviorUtils; 52 import com.android.permissioncontroller.role.utils.SettingsCompat; 53 import com.android.role.controller.model.Role; 54 import com.android.role.controller.model.Roles; 55 import com.android.settingslib.utils.applications.AppUtils; 56 57 import java.util.List; 58 import java.util.Objects; 59 60 /** 61 * Child fragment for a default app. 62 * <p> 63 * Must be added as a child fragment and its parent fragment must be a 64 * {@link PreferenceFragmentCompat} that implements {@link Parent}. 65 * 66 * @param <PF> type of the parent fragment 67 */ 68 public class DefaultAppChildFragment<PF extends PreferenceFragmentCompat 69 & DefaultAppChildFragment.Parent> extends Fragment 70 implements DefaultAppConfirmationDialogFragment.Listener, 71 Preference.OnPreferenceClickListener { 72 73 private static final String PREFERENCE_KEY_RECOMMENDED_CATEGORY = 74 DefaultAppChildFragment.class.getName() + ".preference.RECOMMENDED_CATEGORY"; 75 private static final String PREFERENCE_KEY_OTHERS_CATEGORY = 76 DefaultAppChildFragment.class.getName() + ".preference.OTHERS_CATEGORY"; 77 private static final String PREFERENCE_KEY_NONE = DefaultAppChildFragment.class.getName() 78 + ".preference.NONE"; 79 private static final String PREFERENCE_KEY_DESCRIPTION = DefaultAppChildFragment.class.getName() 80 + ".preference.DESCRIPTION"; 81 private static final String PREFERENCE_KEY_OTHER_NFC_SERVICES = 82 DefaultAppChildFragment.class.getName() + ".preference.OTHER_NFC_SERVICES"; 83 private static final String PREFERENCE_EXTRA_PACKAGE_NAME = 84 DefaultAppChildFragment.class.getName() + ".extra.PACKAGE_NAME"; 85 private static final String PREFERENCE_EXTRA_UID = DefaultAppChildFragment.class.getName() 86 + ".extra.UID"; 87 88 @NonNull 89 private String mRoleName; 90 @NonNull 91 private UserHandle mUser; 92 93 @NonNull 94 private Role mRole; 95 96 @NonNull 97 private DefaultAppViewModel mViewModel; 98 99 /** 100 * Create a new instance of this fragment. 101 * 102 * @param roleName the name of the role for the default app 103 * @param user the user for the default app 104 * 105 * @return a new instance of this fragment 106 */ 107 @NonNull newInstance(@onNull String roleName, @NonNull UserHandle user)108 public static DefaultAppChildFragment newInstance(@NonNull String roleName, 109 @NonNull UserHandle user) { 110 DefaultAppChildFragment fragment = new DefaultAppChildFragment(); 111 Bundle arguments = new Bundle(); 112 arguments.putString(Intent.EXTRA_ROLE_NAME, roleName); 113 arguments.putParcelable(Intent.EXTRA_USER, user); 114 fragment.setArguments(arguments); 115 return fragment; 116 } 117 118 @Override onCreate(@ullable Bundle savedInstanceState)119 public void onCreate(@Nullable Bundle savedInstanceState) { 120 super.onCreate(savedInstanceState); 121 122 Bundle arguments = getArguments(); 123 mRoleName = arguments.getString(Intent.EXTRA_ROLE_NAME); 124 mUser = arguments.getParcelable(Intent.EXTRA_USER); 125 } 126 127 @Override onActivityCreated(@ullable Bundle savedInstanceState)128 public void onActivityCreated(@Nullable Bundle savedInstanceState) { 129 super.onActivityCreated(savedInstanceState); 130 131 PF preferenceFragment = requirePreferenceFragment(); 132 Activity activity = requireActivity(); 133 mRole = Roles.get(activity).get(mRoleName); 134 preferenceFragment.setTitle(getString(mRole.getLabelResource())); 135 136 ViewModelProvider.Factory viewModelFactory = new DefaultAppViewModel.Factory(mRole, mUser, 137 activity.getApplication()); 138 mViewModel = new ViewModelProvider(this, viewModelFactory).get(DefaultAppViewModel.class); 139 mViewModel.getRecommendedLiveData().observe(this, 140 applicationItems -> onApplicationListChanged()); 141 mViewModel.getLiveData().observe(this, applicationItems -> onApplicationListChanged()); 142 mViewModel.getManageRoleHolderStateLiveData().observe(this, 143 this::onManageRoleHolderStateChanged); 144 } 145 onApplicationListChanged()146 private void onApplicationListChanged() { 147 List<RoleApplicationItem> recommendedApplicationItems = 148 mViewModel.getRecommendedLiveData().getValue(); 149 if (recommendedApplicationItems == null) { 150 return; 151 } 152 List<RoleApplicationItem> otherApplicationItems = mViewModel.getLiveData().getValue(); 153 if (otherApplicationItems == null) { 154 return; 155 } 156 157 PF preferenceFragment = requirePreferenceFragment(); 158 PreferenceManager preferenceManager = preferenceFragment.getPreferenceManager(); 159 Context context = preferenceManager.getContext(); 160 161 PreferenceScreen preferenceScreen = preferenceFragment.getPreferenceScreen(); 162 PreferenceCategory oldRecommendedPreferenceCategory = null; 163 PreferenceCategory oldOthersPreferenceCategory = null; 164 ArrayMap<String, Preference> oldPreferences = new ArrayMap<>(); 165 if (preferenceScreen == null) { 166 preferenceScreen = preferenceManager.createPreferenceScreen(context); 167 preferenceFragment.setPreferenceScreen(preferenceScreen); 168 } else { 169 if (Flags.defaultAppsRecommendationEnabled()) { 170 oldRecommendedPreferenceCategory = 171 preferenceScreen.findPreference(PREFERENCE_KEY_RECOMMENDED_CATEGORY); 172 clearPreferenceCategory(oldRecommendedPreferenceCategory, oldPreferences); 173 oldOthersPreferenceCategory = 174 preferenceScreen.findPreference(PREFERENCE_KEY_OTHERS_CATEGORY); 175 clearPreferenceCategory(oldOthersPreferenceCategory, oldPreferences); 176 } 177 clearPreferences(preferenceScreen, oldPreferences); 178 } 179 180 if (Flags.defaultAppsRecommendationEnabled() && !recommendedApplicationItems.isEmpty()) { 181 addApplicationPreferenceCategory(oldRecommendedPreferenceCategory, 182 PREFERENCE_KEY_RECOMMENDED_CATEGORY, 183 getString(R.string.default_app_recommended), preferenceScreen, false, false, 184 recommendedApplicationItems, oldPreferences, context); 185 if (mRole.shouldShowNone() || !otherApplicationItems.isEmpty()) { 186 boolean noneChecked = !(hasHolderApplication(recommendedApplicationItems) 187 || hasHolderApplication(otherApplicationItems)); 188 addApplicationPreferenceCategory(oldOthersPreferenceCategory, 189 PREFERENCE_KEY_OTHERS_CATEGORY, getString(R.string.default_app_others), 190 preferenceScreen, true, noneChecked, otherApplicationItems, oldPreferences, 191 context); 192 } 193 } else { 194 boolean noneChecked = !hasHolderApplication(otherApplicationItems); 195 addNonePreferenceIfNeeded(preferenceScreen, noneChecked, oldPreferences, context); 196 addApplicationPreferences(preferenceScreen, otherApplicationItems, oldPreferences, 197 context); 198 } 199 200 addNonPaymentNfcServicesPreference(preferenceScreen, oldPreferences, context); 201 addDescriptionPreference(preferenceScreen, oldPreferences); 202 203 preferenceFragment.onPreferenceScreenChanged(); 204 } 205 clearPreferenceCategory(@ullable PreferenceCategory preferenceCategory, @NonNull ArrayMap<String, Preference> oldPreferences)206 private static void clearPreferenceCategory(@Nullable PreferenceCategory preferenceCategory, 207 @NonNull ArrayMap<String, Preference> oldPreferences) { 208 if (preferenceCategory == null) { 209 return; 210 } 211 clearPreferences(preferenceCategory, oldPreferences); 212 preferenceCategory.getParent().removePreference(preferenceCategory); 213 preferenceCategory.setOrder(Preference.DEFAULT_ORDER); 214 } 215 clearPreferences(@onNull PreferenceGroup preferenceGroup, @NonNull ArrayMap<String, Preference> oldPreferences)216 private static void clearPreferences(@NonNull PreferenceGroup preferenceGroup, 217 @NonNull ArrayMap<String, Preference> oldPreferences) { 218 for (int i = preferenceGroup.getPreferenceCount() - 1; i >= 0; --i) { 219 Preference preference = preferenceGroup.getPreference(i); 220 221 preferenceGroup.removePreference(preference); 222 preference.setOrder(Preference.DEFAULT_ORDER); 223 oldPreferences.put(preference.getKey(), preference); 224 } 225 } 226 addApplicationPreferenceCategory( @ullable PreferenceCategory oldPreferenceCategory, @NonNull String key, @Nullable String title, @NonNull PreferenceScreen preferenceScreen, boolean addNonePreferenceIfNeeded, boolean noneChecked, @NonNull List<RoleApplicationItem> applicationItems, @NonNull ArrayMap<String, Preference> oldPreferences, @NonNull Context context)227 private void addApplicationPreferenceCategory( 228 @Nullable PreferenceCategory oldPreferenceCategory, @NonNull String key, 229 @Nullable String title, @NonNull PreferenceScreen preferenceScreen, 230 boolean addNonePreferenceIfNeeded, boolean noneChecked, 231 @NonNull List<RoleApplicationItem> applicationItems, 232 @NonNull ArrayMap<String, Preference> oldPreferences, @NonNull Context context) { 233 PreferenceCategory preferenceCategory = oldPreferenceCategory; 234 if (preferenceCategory == null) { 235 preferenceCategory = new PreferenceCategory(context); 236 preferenceCategory.setKey(key); 237 preferenceCategory.setTitle(title); 238 } 239 preferenceScreen.addPreference(preferenceCategory); 240 if (addNonePreferenceIfNeeded) { 241 addNonePreferenceIfNeeded(preferenceCategory, noneChecked, oldPreferences, context); 242 } 243 addApplicationPreferences(preferenceCategory, applicationItems, oldPreferences, context); 244 } 245 hasHolderApplication( @onNull List<RoleApplicationItem> applicationItems)246 private static boolean hasHolderApplication( 247 @NonNull List<RoleApplicationItem> applicationItems) { 248 int applicationItemsSize = applicationItems.size(); 249 for (int i = 0; i < applicationItemsSize; i++) { 250 RoleApplicationItem applicationItem = applicationItems.get(i); 251 if (applicationItem.isHolderApplication()) { 252 return true; 253 } 254 } 255 return false; 256 } 257 addNonePreferenceIfNeeded(@onNull PreferenceGroup preferenceGroup, boolean checked, @NonNull ArrayMap<String, Preference> oldPreferences, @NonNull Context context)258 private void addNonePreferenceIfNeeded(@NonNull PreferenceGroup preferenceGroup, 259 boolean checked, @NonNull ArrayMap<String, Preference> oldPreferences, 260 @NonNull Context context) { 261 if (!mRole.shouldShowNone()) { 262 return; 263 } 264 265 Drawable icon = AppCompatResources.getDrawable(context, R.drawable.ic_remove_circle); 266 String title = getString(R.string.default_app_none); 267 addApplicationPreference(preferenceGroup, PREFERENCE_KEY_NONE, icon, title, checked, null, 268 oldPreferences, context); 269 } 270 addApplicationPreferences(@onNull PreferenceGroup preferenceGroup, @NonNull List<RoleApplicationItem> applicationItems, @NonNull ArrayMap<String, Preference> oldPreferences, @NonNull Context context)271 private void addApplicationPreferences(@NonNull PreferenceGroup preferenceGroup, 272 @NonNull List<RoleApplicationItem> applicationItems, 273 @NonNull ArrayMap<String, Preference> oldPreferences, @NonNull Context context) { 274 int applicationItemsSize = applicationItems.size(); 275 for (int i = 0; i < applicationItemsSize; i++) { 276 RoleApplicationItem applicationItem = applicationItems.get(i); 277 ApplicationInfo applicationInfo = applicationItem.getApplicationInfo(); 278 int userId = UserHandle.getUserHandleForUid(applicationInfo.uid).getIdentifier(); 279 String key = applicationInfo.packageName + "@" + userId; 280 Drawable icon = Utils.getBadgedIcon(context, applicationInfo); 281 String title = Utils.getFullAppLabel(applicationInfo, context); 282 boolean isHolderApplication = applicationItem.isHolderApplication(); 283 284 addApplicationPreference(preferenceGroup, key, icon, title, isHolderApplication, 285 applicationInfo, oldPreferences, context); 286 } 287 } 288 addApplicationPreference(@onNull PreferenceGroup preferenceGroup, @NonNull String key, @NonNull Drawable icon, @NonNull CharSequence title, boolean checked, @Nullable ApplicationInfo applicationInfo, @NonNull ArrayMap<String, Preference> oldPreferences, @NonNull Context context)289 private void addApplicationPreference(@NonNull PreferenceGroup preferenceGroup, 290 @NonNull String key, @NonNull Drawable icon, @NonNull CharSequence title, 291 boolean checked, @Nullable ApplicationInfo applicationInfo, 292 @NonNull ArrayMap<String, Preference> oldPreferences, @NonNull Context context) { 293 RoleApplicationPreference roleApplicationPreference = 294 (RoleApplicationPreference) oldPreferences.get(key); 295 TwoStatePreference preference; 296 if (roleApplicationPreference == null) { 297 roleApplicationPreference = requirePreferenceFragment().createApplicationPreference(); 298 preference = roleApplicationPreference.asTwoStatePreference(); 299 preference.setKey(key); 300 preference.setIcon(icon); 301 preference.setTitle(title); 302 preference.setPersistent(false); 303 preference.setOnPreferenceChangeListener((preference2, newValue) -> false); 304 preference.setOnPreferenceClickListener(this); 305 // In the cases we need this (see #onPreferenceClick()), this should never be null. 306 // This method (addPreference) is used for both legitimate apps and the `NONE` item, 307 // the `NONE` item passes a null applicationinfo object. NFC uses a different preference 308 // method for adding, and a different onclick method 309 if (applicationInfo != null) { 310 UserHandle user = UserHandle.getUserHandleForUid(applicationInfo.uid); 311 roleApplicationPreference.setContentDescription( 312 AppUtils.getAppContentDescription( 313 context, applicationInfo.packageName, user.getIdentifier())); 314 Bundle extras = preference.getExtras(); 315 extras.putString(PREFERENCE_EXTRA_PACKAGE_NAME, applicationInfo.packageName); 316 extras.putInt(PREFERENCE_EXTRA_UID, applicationInfo.uid); 317 } 318 } else { 319 preference = roleApplicationPreference.asTwoStatePreference(); 320 } 321 322 preference.setChecked(checked); 323 if (applicationInfo != null) { 324 UserHandle user = UserHandle.getUserHandleForUid(applicationInfo.uid); 325 roleApplicationPreference.setRestrictionIntent( 326 mRole.getApplicationRestrictionIntentAsUser(applicationInfo, user, context)); 327 RoleUiBehaviorUtils.prepareApplicationPreferenceAsUser(mRole, roleApplicationPreference, 328 applicationInfo, user, context); 329 } 330 331 preferenceGroup.addPreference(preference); 332 } 333 onManageRoleHolderStateChanged(int state)334 private void onManageRoleHolderStateChanged(int state) { 335 ManageRoleHolderStateLiveData liveData = mViewModel.getManageRoleHolderStateLiveData(); 336 switch (state) { 337 case ManageRoleHolderStateLiveData.STATE_SUCCESS: 338 String packageName = liveData.getLastPackageName(); 339 if (packageName != null) { 340 mRole.onHolderSelectedAsUser(packageName, liveData.getLastUser(), 341 requireContext()); 342 } 343 liveData.resetState(); 344 break; 345 case ManageRoleHolderStateLiveData.STATE_FAILURE: 346 liveData.resetState(); 347 break; 348 } 349 } 350 351 @Override onPreferenceClick(@onNull Preference preference)352 public boolean onPreferenceClick(@NonNull Preference preference) { 353 String key = preference.getKey(); 354 if (Objects.equals(key, PREFERENCE_KEY_NONE)) { 355 PermissionControllerStatsLog.write( 356 ROLE_SETTINGS_FRAGMENT_ACTION_REPORTED, Process.INVALID_UID, null, mRoleName); 357 mViewModel.setNoneDefaultApp(); 358 } else { 359 String packageName = 360 preference.getExtras().getString(PREFERENCE_EXTRA_PACKAGE_NAME); 361 int uid = preference.getExtras().getInt(PREFERENCE_EXTRA_UID); 362 CharSequence confirmationMessage = 363 RoleUiBehaviorUtils.getConfirmationMessage(mRole, packageName, 364 requireContext()); 365 if (confirmationMessage != null) { 366 DefaultAppConfirmationDialogFragment.show(packageName, uid, confirmationMessage, 367 this); 368 } else { 369 setDefaultApp(packageName, uid); 370 } 371 } 372 return true; 373 } 374 375 @Override setDefaultApp(@onNull String packageName, int uid)376 public void setDefaultApp(@NonNull String packageName, int uid) { 377 PermissionControllerStatsLog.write( 378 ROLE_SETTINGS_FRAGMENT_ACTION_REPORTED, uid, packageName, mRoleName); 379 mViewModel.setDefaultApp(packageName, UserHandle.getUserHandleForUid(uid)); 380 } 381 addNonPaymentNfcServicesPreference(@onNull PreferenceScreen preferenceScreen, @NonNull ArrayMap<String, Preference> oldPreferences, @NonNull Context context)382 private void addNonPaymentNfcServicesPreference(@NonNull PreferenceScreen preferenceScreen, 383 @NonNull ArrayMap<String, Preference> oldPreferences, @NonNull Context context) { 384 if (!(SdkLevel.isAtLeastV() && Objects.equals(mRoleName, RoleManager.ROLE_WALLET))) { 385 return; 386 } 387 388 Intent intent = new Intent(SettingsCompat.ACTION_MANAGE_OTHER_NFC_SERVICES_SETTINGS); 389 if (!PackageUtils.isIntentResolvedToSettings(intent, context)) { 390 return; 391 } 392 393 Preference preference = oldPreferences.get(PREFERENCE_KEY_OTHER_NFC_SERVICES); 394 if (preference == null) { 395 preference = new Preference(context); 396 preference.setKey(PREFERENCE_KEY_OTHER_NFC_SERVICES); 397 preference.setIcon(AppCompatResources.getDrawable(context, R.drawable.ic_nfc)); 398 preference.setTitle(context.getString( 399 R.string.default_payment_app_other_nfc_services)); 400 preference.setPersistent(false); 401 preference.setOnPreferenceClickListener(preference2 -> { 402 context.startActivity(intent); 403 return true; 404 }); 405 } 406 407 preferenceScreen.addPreference(preference); 408 } 409 addDescriptionPreference(@onNull PreferenceScreen preferenceScreen, @NonNull ArrayMap<String, Preference> oldPreferences)410 private void addDescriptionPreference(@NonNull PreferenceScreen preferenceScreen, 411 @NonNull ArrayMap<String, Preference> oldPreferences) { 412 Preference preference = oldPreferences.get(PREFERENCE_KEY_DESCRIPTION); 413 if (preference == null) { 414 preference = requirePreferenceFragment().createFooterPreference(); 415 preference.setKey(PREFERENCE_KEY_DESCRIPTION); 416 preference.setSummary(mRole.getDescriptionResource()); 417 } 418 419 preferenceScreen.addPreference(preference); 420 } 421 422 @NonNull requirePreferenceFragment()423 private PF requirePreferenceFragment() { 424 //noinspection unchecked 425 return (PF) requireParentFragment(); 426 } 427 428 /** 429 * Interface that the parent fragment must implement. 430 */ 431 public interface Parent { 432 433 /** 434 * Set the title of the current settings page. 435 * 436 * @param title the title of the current settings page 437 */ setTitle(@onNull CharSequence title)438 void setTitle(@NonNull CharSequence title); 439 440 /** 441 * Create a new preference for an application. 442 * 443 * @return a new preference for an application 444 */ 445 @NonNull createApplicationPreference()446 RoleApplicationPreference createApplicationPreference(); 447 448 /** 449 * Create a new preference for the footer. 450 * 451 * @return a new preference for the footer 452 */ 453 @NonNull createFooterPreference()454 Preference createFooterPreference(); 455 456 /** 457 * Callback when changes have been made to the {@link PreferenceScreen} of the parent 458 * {@link PreferenceFragmentCompat}. 459 */ onPreferenceScreenChanged()460 void onPreferenceScreenChanged(); 461 } 462 } 463