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.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.app.Activity; 24 import android.app.Dialog; 25 import android.content.ComponentName; 26 import android.content.ContentResolver; 27 import android.content.Context; 28 import android.content.DialogInterface; 29 import android.content.Intent; 30 import android.content.pm.ApplicationInfo; 31 import android.content.pm.PackageManager; 32 import android.content.pm.ServiceInfo; 33 import android.content.res.Resources; 34 import android.credentials.CredentialManager; 35 import android.credentials.CredentialProviderInfo; 36 import android.credentials.SetEnabledProvidersException; 37 import android.database.ContentObserver; 38 import android.graphics.drawable.Drawable; 39 import android.net.Uri; 40 import android.os.Bundle; 41 import android.os.Handler; 42 import android.os.OutcomeReceiver; 43 import android.os.UserHandle; 44 import android.os.UserManager; 45 import android.provider.Settings; 46 import android.service.autofill.AutofillServiceInfo; 47 import android.text.TextUtils; 48 import android.util.IconDrawableFactory; 49 import android.util.Log; 50 51 import androidx.appcompat.app.AlertDialog; 52 import androidx.core.content.ContextCompat; 53 import androidx.fragment.app.DialogFragment; 54 import androidx.fragment.app.FragmentManager; 55 import androidx.lifecycle.LifecycleObserver; 56 import androidx.lifecycle.LifecycleOwner; 57 import androidx.lifecycle.OnLifecycleEvent; 58 import androidx.preference.Preference; 59 import androidx.preference.PreferenceGroup; 60 import androidx.preference.PreferenceScreen; 61 import androidx.preference.SwitchPreference; 62 63 import com.android.internal.annotations.VisibleForTesting; 64 import com.android.internal.content.PackageMonitor; 65 import com.android.settings.R; 66 import com.android.settings.Utils; 67 import com.android.settings.core.BasePreferenceController; 68 import com.android.settings.dashboard.DashboardFragment; 69 import com.android.settingslib.utils.ThreadUtils; 70 71 import java.util.ArrayList; 72 import java.util.HashMap; 73 import java.util.HashSet; 74 import java.util.List; 75 import java.util.Map; 76 import java.util.Set; 77 import java.util.concurrent.Executor; 78 79 /** Queries available credential manager providers and adds preferences for them. */ 80 public class CredentialManagerPreferenceController extends BasePreferenceController 81 implements LifecycleObserver { 82 public static final String ADD_SERVICE_DEVICE_CONFIG = "credential_manager_service_search_uri"; 83 84 /** 85 * In the settings logic we should hide the list of additional credman providers if there is no 86 * provider selected at the top. The current logic relies on checking whether the autofill 87 * provider is set which won't work for cred-man only providers. Therefore when a CM only 88 * provider is set we will set the autofill setting to be this placeholder. 89 */ 90 public static final String AUTOFILL_CREDMAN_ONLY_PROVIDER_PLACEHOLDER = "credential-provider"; 91 92 private static final String TAG = "CredentialManagerPreferenceController"; 93 private static final String ALTERNATE_INTENT = "android.settings.SYNC_SETTINGS"; 94 private static final String PRIMARY_INTENT = "android.settings.CREDENTIAL_PROVIDER"; 95 private static final int MAX_SELECTABLE_PROVIDERS = 5; 96 97 private final PackageManager mPm; 98 private final IconDrawableFactory mIconFactory; 99 private final List<CredentialProviderInfo> mServices; 100 private final Set<String> mEnabledPackageNames; 101 private final @Nullable CredentialManager mCredentialManager; 102 private final Executor mExecutor; 103 private final Map<String, SwitchPreference> mPrefs = new HashMap<>(); // key is package name 104 private final List<ServiceInfo> mPendingServiceInfos = new ArrayList<>(); 105 private final Handler mHandler = new Handler(); 106 private final SettingContentObserver mSettingsContentObserver; 107 108 private @Nullable FragmentManager mFragmentManager = null; 109 private @Nullable Delegate mDelegate = null; 110 private @Nullable String mFlagOverrideForTest = null; 111 private @Nullable PreferenceScreen mPreferenceScreen = null; 112 113 private boolean mVisibility = false; 114 private boolean mIsWorkProfile = false; 115 CredentialManagerPreferenceController(Context context, String preferenceKey)116 public CredentialManagerPreferenceController(Context context, String preferenceKey) { 117 super(context, preferenceKey); 118 mPm = context.getPackageManager(); 119 mIconFactory = IconDrawableFactory.newInstance(mContext); 120 mServices = new ArrayList<>(); 121 mEnabledPackageNames = new HashSet<>(); 122 mExecutor = ContextCompat.getMainExecutor(mContext); 123 mCredentialManager = 124 getCredentialManager(context, preferenceKey.equals("credentials_test")); 125 mSettingsContentObserver = 126 new SettingContentObserver(mHandler, context.getContentResolver()); 127 mSettingsContentObserver.register(); 128 mSettingsPackageMonitor.register(context, context.getMainLooper(), false); 129 } 130 getCredentialManager(Context context, boolean isTest)131 private @Nullable CredentialManager getCredentialManager(Context context, boolean isTest) { 132 if (isTest) { 133 return null; 134 } 135 136 Object service = context.getSystemService(Context.CREDENTIAL_SERVICE); 137 138 if (service != null && CredentialManager.isServiceEnabled(context)) { 139 return (CredentialManager) service; 140 } 141 142 return null; 143 } 144 145 @Override getAvailabilityStatus()146 public int getAvailabilityStatus() { 147 if (mCredentialManager == null) { 148 return UNSUPPORTED_ON_DEVICE; 149 } 150 151 if (!mVisibility) { 152 return CONDITIONALLY_UNAVAILABLE; 153 } 154 155 if (mServices.isEmpty()) { 156 return CONDITIONALLY_UNAVAILABLE; 157 } 158 159 return AVAILABLE; 160 } 161 162 @VisibleForTesting isConnected()163 public boolean isConnected() { 164 return mCredentialManager != null; 165 } 166 167 /** 168 * Initializes the controller with the parent fragment and adds the controller to observe its 169 * lifecycle. Also stores the fragment manager which is used to open dialogs. 170 * 171 * @param fragment the fragment to use as the parent 172 * @param fragmentManager the fragment manager to use 173 * @param intent the intent used to start the activity 174 * @param delegate the delegate to send results back to 175 * @param isWorkProfile whether this controller is under a work profile user 176 */ init( DashboardFragment fragment, FragmentManager fragmentManager, @Nullable Intent launchIntent, @NonNull Delegate delegate, boolean isWorkProfile)177 public void init( 178 DashboardFragment fragment, 179 FragmentManager fragmentManager, 180 @Nullable Intent launchIntent, 181 @NonNull Delegate delegate, 182 boolean isWorkProfile) { 183 fragment.getSettingsLifecycle().addObserver(this); 184 mFragmentManager = fragmentManager; 185 mIsWorkProfile = isWorkProfile; 186 setDelegate(delegate); 187 verifyReceivedIntent(launchIntent); 188 189 // Recreate the content observers because the user might have changed. 190 mSettingsContentObserver.unregister(); 191 mSettingsContentObserver.register(); 192 } 193 194 /** 195 * Parses and sets the package component name. Returns a boolean as to whether this was 196 * successful. 197 */ 198 @VisibleForTesting verifyReceivedIntent(Intent launchIntent)199 boolean verifyReceivedIntent(Intent launchIntent) { 200 if (launchIntent == null || launchIntent.getAction() == null) { 201 return false; 202 } 203 204 final String action = launchIntent.getAction(); 205 final boolean isCredProviderAction = TextUtils.equals(action, PRIMARY_INTENT); 206 final boolean isExistingAction = TextUtils.equals(action, ALTERNATE_INTENT); 207 final boolean isValid = isCredProviderAction || isExistingAction; 208 209 if (!isValid) { 210 return false; 211 } 212 213 // After this point we have received a set credential manager provider intent 214 // so we should return a cancelled result if the data we got is no good. 215 if (launchIntent.getData() == null) { 216 setActivityResult(Activity.RESULT_CANCELED); 217 return false; 218 } 219 220 String packageName = launchIntent.getData().getSchemeSpecificPart(); 221 if (packageName == null) { 222 setActivityResult(Activity.RESULT_CANCELED); 223 return false; 224 } 225 226 mPendingServiceInfos.clear(); 227 for (CredentialProviderInfo cpi : mServices) { 228 final ServiceInfo serviceInfo = cpi.getServiceInfo(); 229 if (serviceInfo.packageName.equals(packageName)) { 230 mPendingServiceInfos.add(serviceInfo); 231 } 232 } 233 234 // Don't set the result as RESULT_OK here because we should wait for the user to 235 // enable the provider. 236 if (!mPendingServiceInfos.isEmpty()) { 237 return true; 238 } 239 240 setActivityResult(Activity.RESULT_CANCELED); 241 return false; 242 } 243 244 @VisibleForTesting setDelegate(Delegate delegate)245 void setDelegate(Delegate delegate) { 246 mDelegate = delegate; 247 } 248 setActivityResult(int resultCode)249 private void setActivityResult(int resultCode) { 250 if (mDelegate == null) { 251 Log.e(TAG, "Missing delegate"); 252 return; 253 } 254 mDelegate.setActivityResult(resultCode); 255 } 256 handleIntent()257 private void handleIntent() { 258 List<ServiceInfo> pendingServiceInfos = new ArrayList<>(mPendingServiceInfos); 259 mPendingServiceInfos.clear(); 260 if (pendingServiceInfos.isEmpty()) { 261 return; 262 } 263 264 ServiceInfo serviceInfo = pendingServiceInfos.get(0); 265 ApplicationInfo appInfo = serviceInfo.applicationInfo; 266 CharSequence appName = ""; 267 if (appInfo.nonLocalizedLabel != null) { 268 appName = appInfo.loadLabel(mPm); 269 } 270 271 // Stop if there is no name. 272 if (TextUtils.isEmpty(appName)) { 273 return; 274 } 275 276 NewProviderConfirmationDialogFragment fragment = 277 newNewProviderConfirmationDialogFragment( 278 serviceInfo.packageName, appName, /* setActivityResult= */ true); 279 if (fragment == null || mFragmentManager == null) { 280 return; 281 } 282 283 fragment.show(mFragmentManager, NewProviderConfirmationDialogFragment.TAG); 284 } 285 286 @OnLifecycleEvent(ON_CREATE) onCreate(LifecycleOwner lifecycleOwner)287 void onCreate(LifecycleOwner lifecycleOwner) { 288 update(); 289 } 290 update()291 private void update() { 292 if (mCredentialManager == null) { 293 return; 294 } 295 296 setAvailableServices( 297 mCredentialManager.getCredentialProviderServices( 298 getUser(), CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY), 299 null); 300 } 301 updateFromExternal()302 private void updateFromExternal() { 303 update(); 304 305 if (mPreferenceScreen != null) { 306 displayPreference(mPreferenceScreen); 307 } 308 309 if (mDelegate != null) { 310 mDelegate.forceDelegateRefresh(); 311 } 312 } 313 setVisibility(boolean newVisibility)314 private void setVisibility(boolean newVisibility) { 315 if (newVisibility == mVisibility) { 316 return; 317 } 318 319 mVisibility = newVisibility; 320 if (mDelegate != null) { 321 mDelegate.forceDelegateRefresh(); 322 } 323 } 324 325 @VisibleForTesting setAvailableServices( List<CredentialProviderInfo> availableServices, String flagOverrideForTest)326 void setAvailableServices( 327 List<CredentialProviderInfo> availableServices, String flagOverrideForTest) { 328 mFlagOverrideForTest = flagOverrideForTest; 329 mServices.clear(); 330 mServices.addAll(availableServices); 331 332 // If there is a pending dialog then show it. 333 handleIntent(); 334 335 mEnabledPackageNames.clear(); 336 for (CredentialProviderInfo cpi : availableServices) { 337 if (cpi.isEnabled() && !cpi.isPrimary()) { 338 mEnabledPackageNames.add(cpi.getServiceInfo().packageName); 339 } 340 } 341 342 for (String packageName : mPrefs.keySet()) { 343 mPrefs.get(packageName).setChecked(mEnabledPackageNames.contains(packageName)); 344 } 345 } 346 347 @Override displayPreference(PreferenceScreen screen)348 public void displayPreference(PreferenceScreen screen) { 349 super.displayPreference(screen); 350 351 // Since the UI is being cleared, clear any refs. 352 mPrefs.clear(); 353 354 mPreferenceScreen = screen; 355 PreferenceGroup group = screen.findPreference(getPreferenceKey()); 356 group.removeAll(); 357 358 Context context = screen.getContext(); 359 mPrefs.putAll(buildPreferenceList(context, group)); 360 } 361 362 /** 363 * Gets the preference that allows to add a new cred man service. 364 * 365 * @return the pref to be added 366 */ 367 @VisibleForTesting newAddServicePreference(String searchUri, Context context)368 public Preference newAddServicePreference(String searchUri, Context context) { 369 final Intent addNewServiceIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)); 370 final Preference preference = new Preference(context); 371 preference.setOnPreferenceClickListener( 372 p -> { 373 context.startActivityAsUser(addNewServiceIntent, UserHandle.of(getUser())); 374 return true; 375 }); 376 preference.setTitle(R.string.print_menu_item_add_service); 377 preference.setOrder(Integer.MAX_VALUE - 1); 378 preference.setPersistent(false); 379 380 // Try to set the icon this should fail in a test environment but work 381 // in the actual app. 382 try { 383 preference.setIcon(R.drawable.ic_add_24dp); 384 } catch (Resources.NotFoundException e) { 385 Log.e(TAG, "Failed to find icon for add services link", e); 386 } 387 return preference; 388 } 389 390 /** Aggregates the list of services and builds a list of UI prefs to show. */ 391 @VisibleForTesting buildPreferenceList( Context context, PreferenceGroup group)392 public Map<String, SwitchPreference> buildPreferenceList( 393 Context context, PreferenceGroup group) { 394 // Get the selected autofill provider. If it is the placeholder then replace it with an 395 // empty string. 396 String selectedAutofillProvider = 397 DefaultCombinedPicker.getSelectedAutofillProvider(mContext, getUser()); 398 if (TextUtils.equals( 399 selectedAutofillProvider, AUTOFILL_CREDMAN_ONLY_PROVIDER_PLACEHOLDER)) { 400 selectedAutofillProvider = ""; 401 } 402 403 // Get the list of combined providers. 404 List<CombinedProviderInfo> providers = 405 CombinedProviderInfo.buildMergedList( 406 AutofillServiceInfo.getAvailableServices(context, getUser()), 407 mServices, 408 selectedAutofillProvider); 409 410 // Get the provider that is displayed at the top. If there is none then hide 411 // everything. 412 CombinedProviderInfo topProvider = CombinedProviderInfo.getTopProvider(providers); 413 if (topProvider == null) { 414 setVisibility(false); 415 return new HashMap<>(); 416 } 417 418 Map<String, SwitchPreference> output = new HashMap<>(); 419 for (CombinedProviderInfo combinedInfo : providers) { 420 final String packageName = combinedInfo.getApplicationInfo().packageName; 421 422 // If this provider is displayed at the top then we should not show it. 423 if (topProvider != null 424 && topProvider.getApplicationInfo().packageName.equals(packageName)) { 425 continue; 426 } 427 428 // If this is an autofill provider then don't show it here. 429 if (combinedInfo.getCredentialProviderInfos().isEmpty()) { 430 continue; 431 } 432 433 Drawable icon = combinedInfo.getAppIcon(context, getUser()); 434 CharSequence title = combinedInfo.getAppName(context); 435 436 // Build the pref and add it to the output & group. 437 SwitchPreference pref = 438 addProviderPreference( 439 context, title, icon, packageName, combinedInfo.getSettingsSubtitle()); 440 output.put(packageName, pref); 441 group.addPreference(pref); 442 } 443 444 // Set the visibility if we have services. 445 setVisibility(!output.isEmpty()); 446 447 return output; 448 } 449 450 /** Creates a preference object based on the provider info. */ 451 @VisibleForTesting createPreference(Context context, CredentialProviderInfo service)452 public SwitchPreference createPreference(Context context, CredentialProviderInfo service) { 453 CharSequence label = service.getLabel(context); 454 return addProviderPreference( 455 context, 456 label == null ? "" : label, 457 service.getServiceIcon(mContext), 458 service.getServiceInfo().packageName, 459 service.getSettingsSubtitle()); 460 } 461 462 /** 463 * Enables the package name as an enabled credential manager provider. 464 * 465 * @param packageName the package name to enable 466 */ 467 @VisibleForTesting togglePackageNameEnabled(String packageName)468 public boolean togglePackageNameEnabled(String packageName) { 469 if (mEnabledPackageNames.size() >= MAX_SELECTABLE_PROVIDERS) { 470 return false; 471 } else { 472 mEnabledPackageNames.add(packageName); 473 commitEnabledPackages(); 474 return true; 475 } 476 } 477 478 /** 479 * Disables the package name as a credential manager provider. 480 * 481 * @param packageName the package name to disable 482 */ 483 @VisibleForTesting togglePackageNameDisabled(String packageName)484 public void togglePackageNameDisabled(String packageName) { 485 mEnabledPackageNames.remove(packageName); 486 commitEnabledPackages(); 487 } 488 489 /** Returns the enabled credential manager provider package names. */ 490 @VisibleForTesting getEnabledProviders()491 public Set<String> getEnabledProviders() { 492 return mEnabledPackageNames; 493 } 494 495 /** 496 * Returns the enabled credential manager provider flattened component names that can be stored 497 * in the setting. 498 */ 499 @VisibleForTesting getEnabledSettings()500 public List<String> getEnabledSettings() { 501 // Get all the component names that match the enabled package names. 502 List<String> enabledServices = new ArrayList<>(); 503 for (CredentialProviderInfo service : mServices) { 504 ComponentName cn = service.getServiceInfo().getComponentName(); 505 if (mEnabledPackageNames.contains(service.getServiceInfo().packageName)) { 506 enabledServices.add(cn.flattenToString()); 507 } 508 } 509 510 return enabledServices; 511 } 512 addProviderPreference( @onNull Context prefContext, @NonNull CharSequence title, @Nullable Drawable icon, @NonNull String packageName, @Nullable CharSequence subtitle)513 private SwitchPreference addProviderPreference( 514 @NonNull Context prefContext, 515 @NonNull CharSequence title, 516 @Nullable Drawable icon, 517 @NonNull String packageName, 518 @Nullable CharSequence subtitle) { 519 final SwitchPreference pref = new SwitchPreference(prefContext); 520 pref.setTitle(title); 521 pref.setChecked(mEnabledPackageNames.contains(packageName)); 522 523 if (icon != null) { 524 pref.setIcon(Utils.getSafeIcon(icon)); 525 } 526 527 if (subtitle != null) { 528 pref.setSummary(subtitle); 529 } 530 531 pref.setOnPreferenceClickListener( 532 p -> { 533 boolean isChecked = pref.isChecked(); 534 535 if (isChecked) { 536 if (mEnabledPackageNames.size() >= MAX_SELECTABLE_PROVIDERS) { 537 // Show the error if too many enabled. 538 pref.setChecked(false); 539 final DialogFragment fragment = newErrorDialogFragment(); 540 541 if (fragment == null || mFragmentManager == null) { 542 return true; 543 } 544 545 fragment.show(mFragmentManager, ErrorDialogFragment.TAG); 546 return true; 547 } 548 549 togglePackageNameEnabled(packageName); 550 551 // Enable all prefs. 552 if (mPrefs.containsKey(packageName)) { 553 mPrefs.get(packageName).setChecked(true); 554 } 555 return true; 556 } else { 557 togglePackageNameDisabled(packageName); 558 } 559 560 return true; 561 }); 562 563 return pref; 564 } 565 commitEnabledPackages()566 private void commitEnabledPackages() { 567 // Commit using the CredMan API. 568 if (mCredentialManager == null) { 569 return; 570 } 571 572 // Get the existing primary providers since we don't touch them in 573 // this part of the UI we should just copy them over. 574 Set<String> primaryServices = new HashSet<>(); 575 List<String> enabledServices = getEnabledSettings(); 576 for (CredentialProviderInfo service : mServices) { 577 if (service.isPrimary()) { 578 String flattened = service.getServiceInfo().getComponentName().flattenToString(); 579 primaryServices.add(flattened); 580 enabledServices.add(flattened); 581 } 582 } 583 584 mCredentialManager.setEnabledProviders( 585 new ArrayList<>(primaryServices), 586 enabledServices, 587 getUser(), 588 mExecutor, 589 new OutcomeReceiver<Void, SetEnabledProvidersException>() { 590 @Override 591 public void onResult(Void result) { 592 Log.i(TAG, "setEnabledProviders success"); 593 updateFromExternal(); 594 } 595 596 @Override 597 public void onError(SetEnabledProvidersException e) { 598 Log.e(TAG, "setEnabledProviders error: " + e.toString()); 599 } 600 }); 601 } 602 603 /** Create the new provider confirmation dialog. */ 604 private @Nullable NewProviderConfirmationDialogFragment newNewProviderConfirmationDialogFragment( @onNull String packageName, @NonNull CharSequence appName, boolean setActivityResult)605 newNewProviderConfirmationDialogFragment( 606 @NonNull String packageName, 607 @NonNull CharSequence appName, 608 boolean setActivityResult) { 609 DialogHost host = 610 new DialogHost() { 611 @Override 612 public void onDialogClick(int whichButton) { 613 completeEnableProviderDialogBox( 614 whichButton, packageName, setActivityResult); 615 } 616 617 @Override 618 public void onCancel() {} 619 }; 620 621 return new NewProviderConfirmationDialogFragment(host, packageName, appName); 622 } 623 624 @VisibleForTesting completeEnableProviderDialogBox( int whichButton, String packageName, boolean setActivityResult)625 void completeEnableProviderDialogBox( 626 int whichButton, String packageName, boolean setActivityResult) { 627 int activityResult = -1; 628 if (whichButton == DialogInterface.BUTTON_POSITIVE) { 629 if (togglePackageNameEnabled(packageName)) { 630 // Enable all prefs. 631 if (mPrefs.containsKey(packageName)) { 632 mPrefs.get(packageName).setChecked(true); 633 } 634 activityResult = Activity.RESULT_OK; 635 } else { 636 // There are too many providers so set the result as cancelled. 637 activityResult = Activity.RESULT_CANCELED; 638 639 // Show the error if too many enabled. 640 final DialogFragment fragment = newErrorDialogFragment(); 641 642 if (fragment == null || mFragmentManager == null) { 643 return; 644 } 645 646 fragment.show(mFragmentManager, ErrorDialogFragment.TAG); 647 } 648 } else { 649 // The user clicked the cancel button so send that result back. 650 activityResult = Activity.RESULT_CANCELED; 651 } 652 653 // If the dialog is being shown because of the intent we should 654 // return a result. 655 if (activityResult == -1 || !setActivityResult) { 656 setActivityResult(activityResult); 657 } 658 } 659 newErrorDialogFragment()660 private @Nullable ErrorDialogFragment newErrorDialogFragment() { 661 DialogHost host = 662 new DialogHost() { 663 @Override 664 public void onDialogClick(int whichButton) {} 665 666 @Override 667 public void onCancel() {} 668 }; 669 670 return new ErrorDialogFragment(host); 671 } 672 getUser()673 protected int getUser() { 674 if (mIsWorkProfile) { 675 UserHandle workProfile = Utils.getManagedProfile(UserManager.get(mContext)); 676 return workProfile.getIdentifier(); 677 } 678 return UserHandle.myUserId(); 679 } 680 681 /** Called when the dialog button is clicked. */ 682 private static interface DialogHost { onDialogClick(int whichButton)683 void onDialogClick(int whichButton); 684 onCancel()685 void onCancel(); 686 } 687 688 /** Called to send messages back to the parent fragment. */ 689 public static interface Delegate { setActivityResult(int resultCode)690 void setActivityResult(int resultCode); 691 forceDelegateRefresh()692 void forceDelegateRefresh(); 693 } 694 695 /** 696 * Monitor coming and going credman services and calls {@link #DefaultCombinedPicker} when 697 * necessary 698 */ 699 private final PackageMonitor mSettingsPackageMonitor = 700 new PackageMonitor() { 701 @Override 702 public void onPackageAdded(String packageName, int uid) { 703 ThreadUtils.postOnMainThread(() -> updateFromExternal()); 704 } 705 706 @Override 707 public void onPackageModified(String packageName) { 708 ThreadUtils.postOnMainThread(() -> updateFromExternal()); 709 } 710 711 @Override 712 public void onPackageRemoved(String packageName, int uid) { 713 ThreadUtils.postOnMainThread(() -> updateFromExternal()); 714 } 715 }; 716 717 /** Dialog fragment parent class. */ 718 private abstract static class CredentialManagerDialogFragment extends DialogFragment 719 implements DialogInterface.OnClickListener { 720 721 public static final String TAG = "CredentialManagerDialogFragment"; 722 public static final String PACKAGE_NAME_KEY = "package_name"; 723 public static final String APP_NAME_KEY = "app_name"; 724 725 private DialogHost mDialogHost; 726 CredentialManagerDialogFragment(DialogHost dialogHost)727 CredentialManagerDialogFragment(DialogHost dialogHost) { 728 super(); 729 mDialogHost = dialogHost; 730 } 731 getDialogHost()732 public DialogHost getDialogHost() { 733 return mDialogHost; 734 } 735 736 @Override onCancel(@onNull DialogInterface dialog)737 public void onCancel(@NonNull DialogInterface dialog) { 738 getDialogHost().onCancel(); 739 } 740 } 741 742 /** Dialog showing error when too many providers are selected. */ 743 public static class ErrorDialogFragment extends CredentialManagerDialogFragment { 744 ErrorDialogFragment(DialogHost dialogHost)745 ErrorDialogFragment(DialogHost dialogHost) { 746 super(dialogHost); 747 } 748 749 @Override onCreateDialog(Bundle savedInstanceState)750 public Dialog onCreateDialog(Bundle savedInstanceState) { 751 return new AlertDialog.Builder(getActivity()) 752 .setTitle(getContext().getString(R.string.credman_error_message_title)) 753 .setMessage(getContext().getString(R.string.credman_error_message)) 754 .setPositiveButton(android.R.string.ok, this) 755 .create(); 756 } 757 758 @Override onClick(DialogInterface dialog, int which)759 public void onClick(DialogInterface dialog, int which) {} 760 } 761 762 /** 763 * Confirmation dialog fragment shows a dialog to the user to confirm that they would like to 764 * enable the new provider. 765 */ 766 public static class NewProviderConfirmationDialogFragment 767 extends CredentialManagerDialogFragment { 768 NewProviderConfirmationDialogFragment( DialogHost dialogHost, @NonNull String packageName, @NonNull CharSequence appName)769 NewProviderConfirmationDialogFragment( 770 DialogHost dialogHost, @NonNull String packageName, @NonNull CharSequence appName) { 771 super(dialogHost); 772 773 final Bundle argument = new Bundle(); 774 argument.putString(PACKAGE_NAME_KEY, packageName); 775 argument.putCharSequence(APP_NAME_KEY, appName); 776 setArguments(argument); 777 } 778 779 @Override onCreateDialog(Bundle savedInstanceState)780 public Dialog onCreateDialog(Bundle savedInstanceState) { 781 final Bundle bundle = getArguments(); 782 final Context context = getContext(); 783 final CharSequence appName = 784 bundle.getCharSequence(CredentialManagerDialogFragment.APP_NAME_KEY); 785 final String title = 786 context.getString(R.string.credman_enable_confirmation_message_title, appName); 787 final String message = 788 context.getString(R.string.credman_enable_confirmation_message, appName); 789 790 return new AlertDialog.Builder(getActivity()) 791 .setTitle(title) 792 .setMessage(message) 793 .setPositiveButton(android.R.string.ok, this) 794 .setNegativeButton(android.R.string.cancel, this) 795 .create(); 796 } 797 798 @Override onClick(DialogInterface dialog, int which)799 public void onClick(DialogInterface dialog, int which) { 800 getDialogHost().onDialogClick(which); 801 } 802 } 803 804 /** Updates the list if setting content changes. */ 805 private final class SettingContentObserver extends ContentObserver { 806 807 private final Uri mAutofillService = 808 Settings.Secure.getUriFor(Settings.Secure.AUTOFILL_SERVICE); 809 810 private final Uri mCredentialService = 811 Settings.Secure.getUriFor(Settings.Secure.CREDENTIAL_SERVICE); 812 813 private final Uri mCredentialPrimaryService = 814 Settings.Secure.getUriFor(Settings.Secure.CREDENTIAL_SERVICE_PRIMARY); 815 816 private ContentResolver mContentResolver; 817 SettingContentObserver(Handler handler, ContentResolver contentResolver)818 public SettingContentObserver(Handler handler, ContentResolver contentResolver) { 819 super(handler); 820 mContentResolver = contentResolver; 821 } 822 register()823 public void register() { 824 mContentResolver.registerContentObserver(mAutofillService, false, this, getUser()); 825 mContentResolver.registerContentObserver(mCredentialService, false, this, getUser()); 826 mContentResolver.registerContentObserver( 827 mCredentialPrimaryService, false, this, getUser()); 828 } 829 unregister()830 public void unregister() { 831 mContentResolver.unregisterContentObserver(this); 832 } 833 834 @Override onChange(boolean selfChange, Uri uri)835 public void onChange(boolean selfChange, Uri uri) { 836 updateFromExternal(); 837 } 838 } 839 } 840