1 /* 2 * Copyright (C) 2022 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.settings.applications.credentials; 18 19 import static androidx.lifecycle.Lifecycle.Event.ON_CREATE; 20 21 import android.app.Activity; 22 import android.app.Dialog; 23 import android.content.ComponentName; 24 import android.content.ContentResolver; 25 import android.content.Context; 26 import android.content.DialogInterface; 27 import android.content.Intent; 28 import android.content.pm.ApplicationInfo; 29 import android.content.pm.PackageManager; 30 import android.content.pm.ServiceInfo; 31 import android.content.res.Resources; 32 import android.credentials.CredentialManager; 33 import android.credentials.CredentialProviderInfo; 34 import android.credentials.SetEnabledProvidersException; 35 import android.credentials.flags.Flags; 36 import android.database.ContentObserver; 37 import android.graphics.drawable.Drawable; 38 import android.net.Uri; 39 import android.os.Bundle; 40 import android.os.Handler; 41 import android.os.OutcomeReceiver; 42 import android.os.UserHandle; 43 import android.os.UserManager; 44 import android.provider.Settings; 45 import android.service.autofill.AutofillServiceInfo; 46 import android.text.TextUtils; 47 import android.util.Log; 48 import android.util.Pair; 49 import android.view.View; 50 import android.widget.CompoundButton; 51 52 import androidx.annotation.NonNull; 53 import androidx.annotation.Nullable; 54 import androidx.appcompat.app.AlertDialog; 55 import androidx.core.content.ContextCompat; 56 import androidx.fragment.app.DialogFragment; 57 import androidx.fragment.app.FragmentManager; 58 import androidx.lifecycle.LifecycleObserver; 59 import androidx.lifecycle.LifecycleOwner; 60 import androidx.lifecycle.OnLifecycleEvent; 61 import androidx.preference.Preference; 62 import androidx.preference.PreferenceGroup; 63 import androidx.preference.PreferenceScreen; 64 import androidx.preference.PreferenceViewHolder; 65 66 import com.android.internal.annotations.VisibleForTesting; 67 import com.android.internal.content.PackageMonitor; 68 import com.android.settings.R; 69 import com.android.settings.Utils; 70 import com.android.settings.core.BasePreferenceController; 71 import com.android.settings.dashboard.DashboardFragment; 72 import com.android.settingslib.RestrictedLockUtils; 73 import com.android.settingslib.RestrictedPreference; 74 import com.android.settingslib.utils.ThreadUtils; 75 76 import java.util.ArrayList; 77 import java.util.HashMap; 78 import java.util.HashSet; 79 import java.util.List; 80 import java.util.Map; 81 import java.util.Optional; 82 import java.util.Set; 83 import java.util.concurrent.Executor; 84 85 /** Queries available credential manager providers and adds preferences for them. */ 86 public class CredentialManagerPreferenceController extends BasePreferenceController 87 implements LifecycleObserver { 88 public static final String ADD_SERVICE_DEVICE_CONFIG = "credential_manager_service_search_uri"; 89 90 private static final String TAG = "CredentialManagerPreferenceController"; 91 private static final String ALTERNATE_INTENT = "android.settings.SYNC_SETTINGS"; 92 private static final String PRIMARY_INTENT = "android.settings.CREDENTIAL_PROVIDER"; 93 private static final int MAX_SELECTABLE_PROVIDERS = 5; 94 95 /** 96 * In the settings logic we should hide the list of additional credman providers if there is no 97 * provider selected at the top. The current logic relies on checking whether the autofill 98 * provider is set which won't work for cred-man only providers. Therefore when a CM only 99 * provider is set we will set the autofill setting to be this placeholder. 100 */ 101 public static final String AUTOFILL_CREDMAN_ONLY_PROVIDER_PLACEHOLDER = "credential-provider"; 102 103 private final PackageManager mPm; 104 private final List<CredentialProviderInfo> mServices; 105 private final Set<String> mEnabledPackageNames; 106 private final @Nullable CredentialManager mCredentialManager; 107 private final Executor mExecutor; 108 private final Map<String, CombiPreference> mPrefs = new HashMap<>(); // key is package name 109 private final List<ServiceInfo> mPendingServiceInfos = new ArrayList<>(); 110 private final Handler mHandler = new Handler(); 111 private final SettingContentObserver mSettingsContentObserver; 112 private final ImageUtils.IconResizer mIconResizer; 113 114 private @Nullable FragmentManager mFragmentManager = null; 115 private @Nullable Delegate mDelegate = null; 116 private @Nullable String mFlagOverrideForTest = null; 117 private @Nullable PreferenceScreen mPreferenceScreen = null; 118 private @Nullable PreferenceGroup mPreferenceGroup = null; 119 120 private Optional<Boolean> mSimulateHiddenForTests = Optional.empty(); 121 private boolean mIsWorkProfile = false; 122 private boolean mIsPrivateSpace = false; 123 private boolean mSimulateConnectedForTests = false; 124 CredentialManagerPreferenceController(Context context, String preferenceKey)125 public CredentialManagerPreferenceController(Context context, String preferenceKey) { 126 super(context, preferenceKey); 127 mPm = context.getPackageManager(); 128 mServices = new ArrayList<>(); 129 mEnabledPackageNames = new HashSet<>(); 130 mExecutor = ContextCompat.getMainExecutor(mContext); 131 mCredentialManager = 132 getCredentialManager(context, preferenceKey.equals("credentials_test")); 133 mSettingsContentObserver = 134 new SettingContentObserver(mHandler, context.getContentResolver()); 135 mSettingsContentObserver.register(); 136 mSettingsPackageMonitor.register(context, context.getMainLooper(), false); 137 mIconResizer = getResizer(context); 138 } 139 getResizer(Context context)140 private static ImageUtils.IconResizer getResizer(Context context) { 141 final Resources resources = context.getResources(); 142 int size = (int) resources.getDimension(android.R.dimen.app_icon_size); 143 return new ImageUtils.IconResizer(size, size, resources.getDisplayMetrics()); 144 } 145 getCredentialManager(Context context, boolean isTest)146 private @Nullable CredentialManager getCredentialManager(Context context, boolean isTest) { 147 if (isTest) { 148 return null; 149 } 150 151 Object service = context.getSystemService(Context.CREDENTIAL_SERVICE); 152 153 if (service != null && CredentialManager.isServiceEnabled(context)) { 154 return (CredentialManager) service; 155 } 156 157 return null; 158 } 159 160 @Override getAvailabilityStatus()161 public int getAvailabilityStatus() { 162 if (!isConnected()) { 163 return UNSUPPORTED_ON_DEVICE; 164 } 165 166 if (!hasNonPrimaryServices()) { 167 return CONDITIONALLY_UNAVAILABLE; 168 } 169 170 // If we are in work profile mode and there is no user then we 171 // should hide for now. We use CONDITIONALLY_UNAVAILABLE 172 // because it is possible for the user to be set later. 173 if (mIsWorkProfile) { 174 UserHandle workProfile = getWorkProfileUserHandle(); 175 if (workProfile == null) { 176 return CONDITIONALLY_UNAVAILABLE; 177 } 178 } 179 180 return AVAILABLE; 181 } 182 183 @VisibleForTesting isConnected()184 public boolean isConnected() { 185 return mCredentialManager != null || mSimulateConnectedForTests; 186 } 187 setSimulateConnectedForTests(boolean simulateConnectedForTests)188 public void setSimulateConnectedForTests(boolean simulateConnectedForTests) { 189 mSimulateConnectedForTests = simulateConnectedForTests; 190 } 191 192 /** 193 * Initializes the controller with the parent fragment and adds the controller to observe its 194 * lifecycle. Also stores the fragment manager which is used to open dialogs. 195 * 196 * @param fragment the fragment to use as the parent 197 * @param fragmentManager the fragment manager to use 198 * @param intent the intent used to start the activity 199 * @param delegate the delegate to send results back to 200 * @param isWorkProfile whether this controller is under a work profile user 201 */ init( DashboardFragment fragment, FragmentManager fragmentManager, @Nullable Intent launchIntent, @NonNull Delegate delegate, boolean isWorkProfile, boolean isPrivateSpace)202 public void init( 203 DashboardFragment fragment, 204 FragmentManager fragmentManager, 205 @Nullable Intent launchIntent, 206 @NonNull Delegate delegate, 207 boolean isWorkProfile, 208 boolean isPrivateSpace) { 209 fragment.getSettingsLifecycle().addObserver(this); 210 mFragmentManager = fragmentManager; 211 mIsWorkProfile = isWorkProfile; 212 mIsPrivateSpace = isPrivateSpace; 213 214 setDelegate(delegate); 215 verifyReceivedIntent(launchIntent); 216 217 // Recreate the content observers because the user might have changed. 218 mSettingsContentObserver.unregister(); 219 mSettingsContentObserver.register(); 220 221 // When we set the mIsWorkProfile above we should try and force a refresh 222 // so we can get the correct data. 223 delegate.forceDelegateRefresh(); 224 } 225 226 /** 227 * Parses and sets the package component name. Returns a boolean as to whether this was 228 * successful. 229 */ 230 @VisibleForTesting verifyReceivedIntent(Intent launchIntent)231 boolean verifyReceivedIntent(Intent launchIntent) { 232 if (launchIntent == null || launchIntent.getAction() == null) { 233 return false; 234 } 235 236 final String action = launchIntent.getAction(); 237 final boolean isCredProviderAction = TextUtils.equals(action, PRIMARY_INTENT); 238 final boolean isExistingAction = TextUtils.equals(action, ALTERNATE_INTENT); 239 final boolean isValid = isCredProviderAction || isExistingAction; 240 241 if (!isValid) { 242 return false; 243 } 244 245 // After this point we have received a set credential manager provider intent 246 // so we should return a cancelled result if the data we got is no good. 247 if (launchIntent.getData() == null) { 248 setActivityResult(Activity.RESULT_CANCELED); 249 return false; 250 } 251 252 String packageName = launchIntent.getData().getSchemeSpecificPart(); 253 if (packageName == null) { 254 setActivityResult(Activity.RESULT_CANCELED); 255 return false; 256 } 257 258 mPendingServiceInfos.clear(); 259 for (CredentialProviderInfo cpi : mServices) { 260 final ServiceInfo serviceInfo = cpi.getServiceInfo(); 261 if (serviceInfo.packageName.equals(packageName)) { 262 mPendingServiceInfos.add(serviceInfo); 263 } 264 } 265 266 // Don't set the result as RESULT_OK here because we should wait for the user to 267 // enable the provider. 268 if (!mPendingServiceInfos.isEmpty()) { 269 return true; 270 } 271 272 setActivityResult(Activity.RESULT_CANCELED); 273 return false; 274 } 275 276 @VisibleForTesting setDelegate(Delegate delegate)277 void setDelegate(Delegate delegate) { 278 mDelegate = delegate; 279 } 280 setActivityResult(int resultCode)281 private void setActivityResult(int resultCode) { 282 if (mDelegate == null) { 283 Log.e(TAG, "Missing delegate"); 284 return; 285 } 286 mDelegate.setActivityResult(resultCode); 287 } 288 handleIntent()289 private void handleIntent() { 290 List<ServiceInfo> pendingServiceInfos = new ArrayList<>(mPendingServiceInfos); 291 mPendingServiceInfos.clear(); 292 if (pendingServiceInfos.isEmpty()) { 293 return; 294 } 295 296 ServiceInfo serviceInfo = pendingServiceInfos.get(0); 297 ApplicationInfo appInfo = serviceInfo.applicationInfo; 298 CharSequence appName = ""; 299 if (appInfo.nonLocalizedLabel != null) { 300 appName = appInfo.loadLabel(mPm); 301 } 302 303 // Stop if there is no name. 304 if (TextUtils.isEmpty(appName)) { 305 return; 306 } 307 308 NewProviderConfirmationDialogFragment fragment = 309 newNewProviderConfirmationDialogFragment( 310 serviceInfo.packageName, appName, /* shouldSetActivityResult= */ true); 311 if (fragment == null || mFragmentManager == null) { 312 return; 313 } 314 315 fragment.show(mFragmentManager, NewProviderConfirmationDialogFragment.TAG); 316 } 317 318 @OnLifecycleEvent(ON_CREATE) onCreate(LifecycleOwner lifecycleOwner)319 void onCreate(LifecycleOwner lifecycleOwner) { 320 update(); 321 } 322 update()323 private void update() { 324 if (mCredentialManager == null) { 325 return; 326 } 327 328 setAvailableServices( 329 mCredentialManager.getCredentialProviderServices( 330 getUser(), 331 CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_INCLUDING_HIDDEN), 332 null); 333 } 334 buildComponentNameSet( List<CredentialProviderInfo> providers, boolean removeNonPrimary)335 private Set<ComponentName> buildComponentNameSet( 336 List<CredentialProviderInfo> providers, boolean removeNonPrimary) { 337 Set<ComponentName> output = new HashSet<>(); 338 339 for (CredentialProviderInfo cpi : providers) { 340 if (removeNonPrimary && !cpi.isPrimary()) { 341 continue; 342 } 343 344 output.add(cpi.getComponentName()); 345 } 346 347 return output; 348 } 349 updateFromExternal()350 private void updateFromExternal() { 351 if (mCredentialManager == null) { 352 return; 353 } 354 355 // Get the list of new providers and components. 356 setAvailableServices( 357 mCredentialManager.getCredentialProviderServices( 358 getUser(), 359 CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_INCLUDING_HIDDEN), 360 null); 361 362 if (mPreferenceScreen != null) { 363 displayPreference(mPreferenceScreen); 364 } 365 366 if (mDelegate != null) { 367 mDelegate.forceDelegateRefresh(); 368 } 369 } 370 371 @VisibleForTesting forceDelegateRefresh()372 public void forceDelegateRefresh() { 373 if (mDelegate != null) { 374 mDelegate.forceDelegateRefresh(); 375 } 376 } 377 378 @VisibleForTesting setSimulateHiddenForTests(Optional<Boolean> simulateHiddenForTests)379 public void setSimulateHiddenForTests(Optional<Boolean> simulateHiddenForTests) { 380 mSimulateHiddenForTests = simulateHiddenForTests; 381 } 382 383 @VisibleForTesting isHiddenDueToNoProviderSet( Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair)384 public boolean isHiddenDueToNoProviderSet( 385 Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair) { 386 if (mSimulateHiddenForTests.isPresent()) { 387 return mSimulateHiddenForTests.get(); 388 } 389 390 return (providerPair.first.size() == 0 || providerPair.second == null); 391 } 392 393 @VisibleForTesting setAvailableServices( List<CredentialProviderInfo> availableServices, String flagOverrideForTest)394 void setAvailableServices( 395 List<CredentialProviderInfo> availableServices, String flagOverrideForTest) { 396 mFlagOverrideForTest = flagOverrideForTest; 397 mServices.clear(); 398 mServices.addAll(availableServices); 399 400 // If there is a pending dialog then show it. 401 handleIntent(); 402 403 mEnabledPackageNames.clear(); 404 for (CredentialProviderInfo cpi : availableServices) { 405 if (cpi.isEnabled() && !cpi.isPrimary()) { 406 mEnabledPackageNames.add(cpi.getServiceInfo().packageName); 407 } 408 } 409 410 for (String packageName : mPrefs.keySet()) { 411 mPrefs.get(packageName).setChecked(mEnabledPackageNames.contains(packageName)); 412 } 413 } 414 415 @VisibleForTesting hasNonPrimaryServices()416 public boolean hasNonPrimaryServices() { 417 for (CredentialProviderInfo availableService : mServices) { 418 if (!availableService.isPrimary()) { 419 return true; 420 } 421 } 422 423 return false; 424 } 425 426 @Override displayPreference(PreferenceScreen screen)427 public void displayPreference(PreferenceScreen screen) { 428 final String prefKey = getPreferenceKey(); 429 if (TextUtils.isEmpty(prefKey)) { 430 Log.w(TAG, "Skipping displayPreference because key is empty"); 431 return; 432 } 433 434 // Store this reference for later. 435 if (mPreferenceScreen == null) { 436 mPreferenceScreen = screen; 437 mPreferenceGroup = screen.findPreference(prefKey); 438 } 439 440 final Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair = getProviders(); 441 442 maybeUpdateListOfPrefs(providerPair); 443 maybeUpdatePreferenceVisibility(providerPair); 444 } 445 maybeUpdateListOfPrefs( Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair)446 private void maybeUpdateListOfPrefs( 447 Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair) { 448 if (mPreferenceScreen == null || mPreferenceGroup == null) { 449 return; 450 } 451 452 // Build the new list of prefs. 453 Map<String, CombiPreference> newPrefs = 454 buildPreferenceList(mPreferenceScreen.getContext(), providerPair); 455 456 // Determine if we need to update the prefs. 457 Set<String> existingPrefPackageNames = mPrefs.keySet(); 458 if (existingPrefPackageNames.equals(newPrefs.keySet())) { 459 return; 460 } 461 462 // Since the UI is being cleared, clear any refs and prefs. 463 mPrefs.clear(); 464 mPreferenceGroup.removeAll(); 465 466 // Populate the preference list with new data. 467 mPrefs.putAll(newPrefs); 468 for (CombiPreference pref : newPrefs.values()) { 469 mPreferenceGroup.addPreference(pref); 470 } 471 } 472 maybeUpdatePreferenceVisibility( Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair)473 private void maybeUpdatePreferenceVisibility( 474 Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair) { 475 if (mPreferenceScreen == null || mPreferenceGroup == null) { 476 return; 477 } 478 479 final boolean isAvailable = 480 (getAvailabilityStatus() == AVAILABLE) && !isHiddenDueToNoProviderSet(providerPair); 481 482 if (isAvailable) { 483 mPreferenceScreen.addPreference(mPreferenceGroup); 484 mPreferenceGroup.setVisible(true); 485 } else { 486 mPreferenceScreen.removePreference(mPreferenceGroup); 487 mPreferenceGroup.setVisible(false); 488 } 489 } 490 491 /** 492 * Gets the preference that allows to add a new cred man service. 493 * 494 * @return the pref to be added 495 */ 496 @VisibleForTesting newAddServicePreference(String searchUri, Context context)497 public Preference newAddServicePreference(String searchUri, Context context) { 498 final Intent addNewServiceIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)); 499 final Preference preference = new Preference(context); 500 preference.setOnPreferenceClickListener( 501 p -> { 502 context.startActivityAsUser(addNewServiceIntent, 503 UserHandle.of(getUser())); 504 return true; 505 }); 506 preference.setTitle(R.string.print_menu_item_add_service); 507 preference.setOrder(Integer.MAX_VALUE - 1); 508 preference.setPersistent(false); 509 510 // Try to set the icon this should fail in a test environment but work 511 // in the actual app. 512 try { 513 preference.setIcon(R.drawable.ic_add_24dp); 514 } catch (Resources.NotFoundException e) { 515 Log.e(TAG, "Failed to find icon for add services link", e); 516 } 517 return preference; 518 } 519 520 /** 521 * Returns a pair that contains a list of the providers in the first position and the top 522 * provider in the second position. 523 */ getProviders()524 private Pair<List<CombinedProviderInfo>, CombinedProviderInfo> getProviders() { 525 // Get the selected autofill provider. If it is the placeholder then replace it with an 526 // empty string. 527 String selectedAutofillProvider = 528 getSelectedAutofillProvider(mContext, getUser(), TAG); 529 530 // Get the list of combined providers. 531 List<CombinedProviderInfo> providers = 532 CombinedProviderInfo.buildMergedList( 533 AutofillServiceInfo.getAvailableServices(mContext, getUser()), 534 mServices, 535 selectedAutofillProvider); 536 return new Pair<>(providers, CombinedProviderInfo.getTopProvider(providers)); 537 } 538 539 /** Aggregates the list of services and builds a list of UI prefs to show. */ 540 @VisibleForTesting buildPreferenceList( @onNull Context context, @NonNull Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair)541 public @NonNull Map<String, CombiPreference> buildPreferenceList( 542 @NonNull Context context, 543 @NonNull Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair) { 544 // Extract the values. 545 CombinedProviderInfo topProvider = providerPair.second; 546 List<CombinedProviderInfo> providers = providerPair.first; 547 548 // If the provider is set to "none" or there are no providers then we should not 549 // return any providers. 550 if (isHiddenDueToNoProviderSet(providerPair)) { 551 forceDelegateRefresh(); 552 return new HashMap<>(); 553 } 554 555 Map<String, CombiPreference> output = new HashMap<>(); 556 for (CombinedProviderInfo combinedInfo : providers) { 557 final String packageName = combinedInfo.getApplicationInfo().packageName; 558 559 // If this provider is displayed at the top then we should not show it. 560 if (topProvider != null 561 && topProvider.getApplicationInfo() != null 562 && topProvider.getApplicationInfo().packageName.equals(packageName)) { 563 continue; 564 } 565 566 // If this is an autofill provider then don't show it here. 567 if (combinedInfo.getCredentialProviderInfos().isEmpty()) { 568 continue; 569 } 570 571 Drawable icon = combinedInfo.getAppIcon(context, getUser()); 572 CharSequence title = combinedInfo.getAppName(context); 573 574 // Build the pref and add it to the output & group. 575 CombiPreference pref = 576 addProviderPreference( 577 context, 578 title == null ? "" : title, 579 icon, 580 packageName, 581 combinedInfo.getSettingsSubtitle(), 582 combinedInfo.getSettingsActivity(), 583 combinedInfo.getDeviceAdminRestrictions(context, getUser())); 584 output.put(packageName, pref); 585 } 586 587 // Set the visibility if we have services. 588 forceDelegateRefresh(); 589 590 return output; 591 } 592 593 /** Creates a preference object based on the provider info. */ 594 @VisibleForTesting createPreference(Context context, CredentialProviderInfo service)595 public CombiPreference createPreference(Context context, CredentialProviderInfo service) { 596 CharSequence label = service.getLabel(context); 597 return addProviderPreference( 598 context, 599 label == null ? "" : label, 600 service.getServiceIcon(mContext), 601 service.getServiceInfo().packageName, 602 service.getSettingsSubtitle(), 603 service.getSettingsActivity(), 604 /* enforcedCredManAdmin= */ null); 605 } 606 607 /** 608 * Enables the package name as an enabled credential manager provider. 609 * 610 * @param packageName the package name to enable 611 */ 612 @VisibleForTesting togglePackageNameEnabled(String packageName)613 public boolean togglePackageNameEnabled(String packageName) { 614 if (hasProviderLimitBeenReached()) { 615 return false; 616 } else { 617 mEnabledPackageNames.add(packageName); 618 commitEnabledPackages(); 619 return true; 620 } 621 } 622 623 /** 624 * Disables the package name as a credential manager provider. 625 * 626 * @param packageName the package name to disable 627 */ 628 @VisibleForTesting togglePackageNameDisabled(String packageName)629 public void togglePackageNameDisabled(String packageName) { 630 mEnabledPackageNames.remove(packageName); 631 commitEnabledPackages(); 632 } 633 634 /** Returns the enabled credential manager provider package names. */ 635 @VisibleForTesting getEnabledProviders()636 public Set<String> getEnabledProviders() { 637 return mEnabledPackageNames; 638 } 639 640 /** 641 * Returns the enabled credential manager provider flattened component names that can be stored 642 * in the setting. 643 */ 644 @VisibleForTesting getEnabledSettings()645 public List<String> getEnabledSettings() { 646 // Get all the component names that match the enabled package names. 647 List<String> enabledServices = new ArrayList<>(); 648 for (CredentialProviderInfo service : mServices) { 649 ComponentName cn = service.getServiceInfo().getComponentName(); 650 if (mEnabledPackageNames.contains(service.getServiceInfo().packageName)) { 651 enabledServices.add(cn.flattenToString()); 652 } 653 } 654 655 return enabledServices; 656 } 657 658 @VisibleForTesting processIcon(@ullable Drawable icon)659 public @NonNull Drawable processIcon(@Nullable Drawable icon) { 660 // If we didn't get an icon then we should use the default app icon. 661 if (icon == null) { 662 icon = mPm.getDefaultActivityIcon(); 663 } 664 665 Drawable providerIcon = Utils.getSafeIcon(icon); 666 return mIconResizer.createIconThumbnail(providerIcon); 667 } 668 hasProviderLimitBeenReached()669 private boolean hasProviderLimitBeenReached() { 670 return hasProviderLimitBeenReached(mEnabledPackageNames.size()); 671 } 672 673 @VisibleForTesting hasProviderLimitBeenReached(int enabledAdditionalProviderCount)674 public static boolean hasProviderLimitBeenReached(int enabledAdditionalProviderCount) { 675 // If the number of package names has reached the maximum limit then 676 // we should stop any new packages from being added. We will also 677 // reserve one place for the primary provider so if the max limit is 678 // five providers this will be four additional plus the primary. 679 return (enabledAdditionalProviderCount + 1) >= MAX_SELECTABLE_PROVIDERS; 680 } 681 682 /** Gets the credential autofill service component name. */ getCredentialAutofillService(Context context, String tag)683 public static String getCredentialAutofillService(Context context, String tag) { 684 try { 685 return context.getResources().getString( 686 com.android.internal.R.string.config_defaultCredentialManagerAutofillService); 687 } catch (Resources.NotFoundException e) { 688 Log.e(tag, "Failed to find credential autofill service.", e); 689 } 690 return ""; 691 } 692 693 /** Gets the selected autofill provider name. This will filter out place holder names. **/ getSelectedAutofillProvider( Context context, int userId, String tag)694 public static @Nullable String getSelectedAutofillProvider( 695 Context context, int userId, String tag) { 696 String providerName = Settings.Secure.getStringForUser( 697 context.getContentResolver(), Settings.Secure.AUTOFILL_SERVICE, userId); 698 699 if (TextUtils.isEmpty(providerName)) { 700 return providerName; 701 } 702 703 if (providerName.equals(AUTOFILL_CREDMAN_ONLY_PROVIDER_PLACEHOLDER)) { 704 return ""; 705 } 706 707 String credentialAutofillService = ""; 708 if (android.service.autofill.Flags.autofillCredmanDevIntegration()) { 709 credentialAutofillService = getCredentialAutofillService(context, tag); 710 } 711 if (providerName.equals(credentialAutofillService)) { 712 return ""; 713 } 714 715 return providerName; 716 } 717 addProviderPreference( @onNull Context prefContext, @NonNull CharSequence title, @Nullable Drawable icon, @NonNull String packageName, @Nullable CharSequence subtitle, @Nullable CharSequence settingsActivity, @Nullable RestrictedLockUtils.EnforcedAdmin enforcedCredManAdmin)718 private CombiPreference addProviderPreference( 719 @NonNull Context prefContext, 720 @NonNull CharSequence title, 721 @Nullable Drawable icon, 722 @NonNull String packageName, 723 @Nullable CharSequence subtitle, 724 @Nullable CharSequence settingsActivity, 725 @Nullable RestrictedLockUtils.EnforcedAdmin enforcedCredManAdmin) { 726 final CombiPreference pref = 727 new CombiPreference(prefContext, mEnabledPackageNames.contains(packageName)); 728 pref.setTitle(title); 729 pref.setLayoutResource(R.layout.preference_icon_credman); 730 731 if (Flags.newSettingsUi()) { 732 pref.setIcon(processIcon(icon)); 733 } else if (icon != null) { 734 pref.setIcon(icon); 735 } 736 737 if (subtitle != null) { 738 pref.setSummary(subtitle); 739 } 740 741 pref.setDisabledByAdmin(enforcedCredManAdmin); 742 743 pref.setPreferenceListener( 744 new CombiPreference.OnCombiPreferenceClickListener() { 745 @Override 746 public boolean onCheckChanged(CombiPreference p, boolean isChecked) { 747 if (isChecked) { 748 if (hasProviderLimitBeenReached()) { 749 // Show the error if too many enabled. 750 final DialogFragment fragment = newErrorDialogFragment(); 751 752 if (fragment == null || mFragmentManager == null) { 753 return false; 754 } 755 756 fragment.show(mFragmentManager, ErrorDialogFragment.TAG); 757 return false; 758 } 759 760 togglePackageNameEnabled(packageName); 761 762 // Enable all prefs. 763 if (mPrefs.containsKey(packageName)) { 764 mPrefs.get(packageName).setChecked(true); 765 } 766 } else { 767 togglePackageNameDisabled(packageName); 768 } 769 770 return true; 771 } 772 773 @Override 774 public void onLeftSideClicked() { 775 CombinedProviderInfo.launchSettingsActivityIntent( 776 mContext, packageName, settingsActivity, getUser()); 777 } 778 }); 779 780 return pref; 781 } 782 commitEnabledPackages()783 private void commitEnabledPackages() { 784 // Commit using the CredMan API. 785 if (mCredentialManager == null) { 786 return; 787 } 788 789 // Get the existing primary providers since we don't touch them in 790 // this part of the UI we should just copy them over. 791 Set<String> primaryServices = new HashSet<>(); 792 List<String> enabledServices = getEnabledSettings(); 793 for (CredentialProviderInfo service : mServices) { 794 if (service.isPrimary()) { 795 String flattened = service.getServiceInfo().getComponentName().flattenToString(); 796 primaryServices.add(flattened); 797 enabledServices.add(flattened); 798 } 799 } 800 801 mCredentialManager.setEnabledProviders( 802 new ArrayList<>(primaryServices), 803 enabledServices, 804 getUser(), 805 mExecutor, 806 new OutcomeReceiver<Void, SetEnabledProvidersException>() { 807 @Override 808 public void onResult(Void result) { 809 Log.i(TAG, "setEnabledProviders success"); 810 updateFromExternal(); 811 } 812 813 @Override 814 public void onError(SetEnabledProvidersException e) { 815 Log.e(TAG, "setEnabledProviders error: " + e.toString()); 816 } 817 }); 818 } 819 820 /** Create the new provider confirmation dialog. */ 821 private @Nullable NewProviderConfirmationDialogFragment newNewProviderConfirmationDialogFragment( @onNull String packageName, @NonNull CharSequence appName, boolean shouldSetActivityResult)822 newNewProviderConfirmationDialogFragment( 823 @NonNull String packageName, 824 @NonNull CharSequence appName, 825 boolean shouldSetActivityResult) { 826 DialogHost host = 827 new DialogHost() { 828 @Override 829 public void onDialogClick(int whichButton) { 830 completeEnableProviderDialogBox( 831 whichButton, packageName, shouldSetActivityResult); 832 } 833 834 @Override 835 public void onCancel() {} 836 }; 837 838 return new NewProviderConfirmationDialogFragment(host, packageName, appName); 839 } 840 841 @VisibleForTesting completeEnableProviderDialogBox( int whichButton, String packageName, boolean shouldSetActivityResult)842 int completeEnableProviderDialogBox( 843 int whichButton, String packageName, boolean shouldSetActivityResult) { 844 int activityResult = -1; 845 if (whichButton == DialogInterface.BUTTON_POSITIVE) { 846 if (togglePackageNameEnabled(packageName)) { 847 // Enable all prefs. 848 if (mPrefs.containsKey(packageName)) { 849 mPrefs.get(packageName).setChecked(true); 850 } 851 activityResult = Activity.RESULT_OK; 852 } else { 853 // There are too many providers so set the result as cancelled. 854 activityResult = Activity.RESULT_CANCELED; 855 856 // Show the error if too many enabled. 857 final DialogFragment fragment = newErrorDialogFragment(); 858 859 if (fragment == null || mFragmentManager == null) { 860 return activityResult; 861 } 862 863 fragment.show(mFragmentManager, ErrorDialogFragment.TAG); 864 } 865 } else { 866 // The user clicked the cancel button so send that result back. 867 activityResult = Activity.RESULT_CANCELED; 868 } 869 870 // If the dialog is being shown because of the intent we should 871 // return a result. 872 if (activityResult == -1 || !shouldSetActivityResult) { 873 setActivityResult(activityResult); 874 } 875 876 return activityResult; 877 } 878 newErrorDialogFragment()879 private @Nullable ErrorDialogFragment newErrorDialogFragment() { 880 DialogHost host = 881 new DialogHost() { 882 @Override 883 public void onDialogClick(int whichButton) {} 884 885 @Override 886 public void onCancel() {} 887 }; 888 889 return new ErrorDialogFragment(host); 890 } 891 getUser()892 protected int getUser() { 893 return UserUtils.getUser(mIsWorkProfile, mIsPrivateSpace, mContext); 894 } 895 getWorkProfileUserHandle()896 private @Nullable UserHandle getWorkProfileUserHandle() { 897 if (mIsWorkProfile) { 898 return UserUtils.getManagedProfile(UserManager.get(mContext)); 899 } 900 901 return null; 902 } 903 904 /** Called when the dialog button is clicked. */ 905 private static interface DialogHost { onDialogClick(int whichButton)906 void onDialogClick(int whichButton); 907 onCancel()908 void onCancel(); 909 } 910 911 /** Called to send messages back to the parent fragment. */ 912 public static interface Delegate { setActivityResult(int resultCode)913 void setActivityResult(int resultCode); 914 forceDelegateRefresh()915 void forceDelegateRefresh(); 916 } 917 918 /** 919 * Monitor coming and going credman services and calls {@link #DefaultCombinedPicker} when 920 * necessary 921 */ 922 private final PackageMonitor mSettingsPackageMonitor = 923 new PackageMonitor() { 924 @Override 925 public void onPackageAdded(String packageName, int uid) { 926 ThreadUtils.postOnMainThread(() -> updateFromExternal()); 927 } 928 929 @Override 930 public void onPackageModified(String packageName) { 931 ThreadUtils.postOnMainThread(() -> updateFromExternal()); 932 } 933 934 @Override 935 public void onPackageRemoved(String packageName, int uid) { 936 ThreadUtils.postOnMainThread(() -> updateFromExternal()); 937 } 938 }; 939 940 /** Dialog fragment parent class. */ 941 private abstract static class CredentialManagerDialogFragment extends DialogFragment 942 implements DialogInterface.OnClickListener { 943 944 public static final String TAG = "CredentialManagerDialogFragment"; 945 public static final String PACKAGE_NAME_KEY = "package_name"; 946 public static final String APP_NAME_KEY = "app_name"; 947 948 private DialogHost mDialogHost; 949 CredentialManagerDialogFragment(DialogHost dialogHost)950 CredentialManagerDialogFragment(DialogHost dialogHost) { 951 super(); 952 mDialogHost = dialogHost; 953 } 954 getDialogHost()955 public DialogHost getDialogHost() { 956 return mDialogHost; 957 } 958 959 @Override onCancel(@onNull DialogInterface dialog)960 public void onCancel(@NonNull DialogInterface dialog) { 961 getDialogHost().onCancel(); 962 } 963 } 964 965 /** Dialog showing error when too many providers are selected. */ 966 public static class ErrorDialogFragment extends CredentialManagerDialogFragment { 967 ErrorDialogFragment(DialogHost dialogHost)968 ErrorDialogFragment(DialogHost dialogHost) { 969 super(dialogHost); 970 } 971 972 @Override onCreateDialog(Bundle savedInstanceState)973 public Dialog onCreateDialog(Bundle savedInstanceState) { 974 return new AlertDialog.Builder(getActivity()) 975 .setTitle( 976 getContext() 977 .getString( 978 Flags.newSettingsUi() 979 ? R.string.credman_limit_error_msg_title 980 : R.string.credman_error_message_title)) 981 .setMessage( 982 getContext() 983 .getString( 984 Flags.newSettingsUi() 985 ? R.string.credman_limit_error_msg 986 : R.string.credman_error_message)) 987 .setPositiveButton(android.R.string.ok, this) 988 .create(); 989 } 990 991 @Override onClick(DialogInterface dialog, int which)992 public void onClick(DialogInterface dialog, int which) {} 993 } 994 995 /** 996 * Confirmation dialog fragment shows a dialog to the user to confirm that they would like to 997 * enable the new provider. 998 */ 999 public static class NewProviderConfirmationDialogFragment 1000 extends CredentialManagerDialogFragment { 1001 NewProviderConfirmationDialogFragment( DialogHost dialogHost, @NonNull String packageName, @NonNull CharSequence appName)1002 NewProviderConfirmationDialogFragment( 1003 DialogHost dialogHost, @NonNull String packageName, @NonNull CharSequence appName) { 1004 super(dialogHost); 1005 1006 final Bundle argument = new Bundle(); 1007 argument.putString(PACKAGE_NAME_KEY, packageName); 1008 argument.putCharSequence(APP_NAME_KEY, appName); 1009 setArguments(argument); 1010 } 1011 1012 @Override onCreateDialog(Bundle savedInstanceState)1013 public Dialog onCreateDialog(Bundle savedInstanceState) { 1014 final Bundle bundle = getArguments(); 1015 final Context context = getContext(); 1016 final CharSequence appName = 1017 bundle.getCharSequence(CredentialManagerDialogFragment.APP_NAME_KEY); 1018 final String title = 1019 context.getString(R.string.credman_enable_confirmation_message_title, appName); 1020 final String message = 1021 context.getString(R.string.credman_enable_confirmation_message, appName); 1022 1023 return new AlertDialog.Builder(getActivity()) 1024 .setTitle(title) 1025 .setMessage(message) 1026 .setPositiveButton(android.R.string.ok, this) 1027 .setNegativeButton(android.R.string.cancel, this) 1028 .create(); 1029 } 1030 1031 @Override onClick(DialogInterface dialog, int which)1032 public void onClick(DialogInterface dialog, int which) { 1033 getDialogHost().onDialogClick(which); 1034 } 1035 } 1036 1037 /** Updates the list if setting content changes. */ 1038 private final class SettingContentObserver extends ContentObserver { 1039 1040 private final Uri mAutofillService = 1041 Settings.Secure.getUriFor(Settings.Secure.AUTOFILL_SERVICE); 1042 1043 private final Uri mCredentialService = 1044 Settings.Secure.getUriFor(Settings.Secure.CREDENTIAL_SERVICE); 1045 1046 private final Uri mCredentialPrimaryService = 1047 Settings.Secure.getUriFor(Settings.Secure.CREDENTIAL_SERVICE_PRIMARY); 1048 1049 private ContentResolver mContentResolver; 1050 SettingContentObserver(Handler handler, ContentResolver contentResolver)1051 public SettingContentObserver(Handler handler, ContentResolver contentResolver) { 1052 super(handler); 1053 mContentResolver = contentResolver; 1054 } 1055 register()1056 public void register() { 1057 mContentResolver.registerContentObserver(mAutofillService, false, this, getUser()); 1058 mContentResolver.registerContentObserver(mCredentialService, false, this, getUser()); 1059 mContentResolver.registerContentObserver( 1060 mCredentialPrimaryService, false, this, getUser()); 1061 } 1062 unregister()1063 public void unregister() { 1064 mContentResolver.unregisterContentObserver(this); 1065 } 1066 1067 @Override onChange(boolean selfChange, Uri uri)1068 public void onChange(boolean selfChange, Uri uri) { 1069 updateFromExternal(); 1070 } 1071 } 1072 1073 /** CombiPreference is a combination of RestrictedPreference and SwitchPreference. */ 1074 public static class CombiPreference extends RestrictedPreference { 1075 1076 private final Listener mListener = new Listener(); 1077 1078 private class Listener implements View.OnClickListener { 1079 @Override onClick(View buttonView)1080 public void onClick(View buttonView) { 1081 // Forward the event. 1082 if (mSwitch != null && mOnClickListener != null) { 1083 if (!mOnClickListener.onCheckChanged( 1084 CombiPreference.this, mSwitch.isChecked())) { 1085 // The update was not successful since there were too 1086 // many enabled providers to manually reset any state. 1087 mChecked = false; 1088 mSwitch.setChecked(false); 1089 } 1090 } 1091 } 1092 } 1093 1094 // Stores a reference to the switch view. 1095 private @Nullable CompoundButton mSwitch; 1096 1097 // Switch text for on and off states 1098 private @NonNull boolean mChecked = false; 1099 private @Nullable OnCombiPreferenceClickListener mOnClickListener = null; 1100 1101 public interface OnCombiPreferenceClickListener { 1102 /** Called when the check is updated */ onCheckChanged(CombiPreference p, boolean isChecked)1103 boolean onCheckChanged(CombiPreference p, boolean isChecked); 1104 1105 /** Called when the left side is clicked. */ onLeftSideClicked()1106 void onLeftSideClicked(); 1107 } 1108 CombiPreference(Context context, boolean initialValue)1109 public CombiPreference(Context context, boolean initialValue) { 1110 super(context); 1111 mChecked = initialValue; 1112 } 1113 1114 /** Set the new checked value */ setChecked(boolean isChecked)1115 public void setChecked(boolean isChecked) { 1116 // Don't update if we don't need too. 1117 if (mChecked == isChecked) { 1118 return; 1119 } 1120 1121 mChecked = isChecked; 1122 1123 if (mSwitch != null) { 1124 mSwitch.setChecked(isChecked); 1125 } 1126 } 1127 1128 @VisibleForTesting isChecked()1129 public boolean isChecked() { 1130 return mChecked; 1131 } 1132 1133 @Override setTitle(@ullable CharSequence title)1134 public void setTitle(@Nullable CharSequence title) { 1135 super.setTitle(title); 1136 maybeUpdateContentDescription(); 1137 } 1138 maybeUpdateContentDescription()1139 private void maybeUpdateContentDescription() { 1140 final CharSequence appName = getTitle(); 1141 1142 if (mSwitch != null && !TextUtils.isEmpty(appName)) { 1143 mSwitch.setContentDescription( 1144 getContext() 1145 .getString( 1146 R.string.credman_on_off_switch_content_description, 1147 appName)); 1148 } 1149 } 1150 setPreferenceListener(OnCombiPreferenceClickListener onClickListener)1151 public void setPreferenceListener(OnCombiPreferenceClickListener onClickListener) { 1152 mOnClickListener = onClickListener; 1153 } 1154 1155 @Override getSecondTargetResId()1156 protected int getSecondTargetResId() { 1157 return com.android.settingslib.R.layout.preference_widget_primary_switch; 1158 } 1159 1160 @Override onBindViewHolder(PreferenceViewHolder view)1161 public void onBindViewHolder(PreferenceViewHolder view) { 1162 super.onBindViewHolder(view); 1163 1164 // Setup the switch. 1165 View checkableView = 1166 view.itemView.findViewById(com.android.settingslib.R.id.switchWidget); 1167 if (checkableView instanceof CompoundButton switchView) { 1168 switchView.setChecked(mChecked); 1169 switchView.setOnClickListener(mListener); 1170 1171 // Store this for later. 1172 mSwitch = switchView; 1173 1174 // Update the content description. 1175 maybeUpdateContentDescription(); 1176 } 1177 1178 super.setOnPreferenceClickListener( 1179 new Preference.OnPreferenceClickListener() { 1180 @Override 1181 public boolean onPreferenceClick(Preference preference) { 1182 if (mOnClickListener != null) { 1183 mOnClickListener.onLeftSideClicked(); 1184 } 1185 1186 return true; 1187 } 1188 }); 1189 } 1190 } 1191 } 1192