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