1 /* 2 * Copyright (C) 2023 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 com.android.settings.applications.credentials.CredentialManagerPreferenceController.getCredentialAutofillService; 20 21 import android.app.Activity; 22 import android.app.settings.SettingsEnums; 23 import android.content.Context; 24 import android.content.DialogInterface; 25 import android.content.Intent; 26 import android.content.pm.ApplicationInfo; 27 import android.content.pm.PackageItemInfo; 28 import android.content.pm.PackageManager; 29 import android.content.pm.ServiceInfo; 30 import android.credentials.CredentialManager; 31 import android.credentials.CredentialProviderInfo; 32 import android.credentials.SetEnabledProvidersException; 33 import android.credentials.flags.Flags; 34 import android.net.Uri; 35 import android.os.Bundle; 36 import android.os.Handler; 37 import android.os.Looper; 38 import android.os.OutcomeReceiver; 39 import android.os.UserHandle; 40 import android.provider.Settings; 41 import android.service.autofill.AutofillServiceInfo; 42 import android.text.Html; 43 import android.text.TextUtils; 44 import android.util.Log; 45 46 import androidx.annotation.NonNull; 47 import androidx.annotation.Nullable; 48 import androidx.core.content.ContextCompat; 49 import androidx.preference.Preference; 50 51 import com.android.internal.content.PackageMonitor; 52 import com.android.settings.R; 53 import com.android.settings.applications.defaultapps.DefaultAppPickerFragment; 54 import com.android.settingslib.RestrictedSelectorWithWidgetPreference; 55 import com.android.settingslib.applications.DefaultAppInfo; 56 import com.android.settingslib.widget.CandidateInfo; 57 import com.android.settingslib.widget.SelectorWithWidgetPreference; 58 59 import java.util.ArrayList; 60 import java.util.List; 61 62 public class DefaultCombinedPicker extends DefaultAppPickerFragment { 63 64 private boolean mIsWorkProfile; 65 private boolean mIsPrivateSpace; 66 private static final String TAG = "DefaultCombinedPicker"; 67 68 public static final String AUTOFILL_SETTING = Settings.Secure.AUTOFILL_SERVICE; 69 public static final String CREDENTIAL_SETTING = Settings.Secure.CREDENTIAL_SERVICE; 70 71 /** Extra set when the fragment is implementing ACTION_REQUEST_SET_AUTOFILL_SERVICE. */ 72 public static final String EXTRA_PACKAGE_NAME = "package_name"; 73 74 /** Set when the fragment is implementing ACTION_REQUEST_SET_AUTOFILL_SERVICE. */ 75 private DialogInterface.OnClickListener mCancelListener; 76 77 private CredentialManager mCredentialManager; 78 private int mIntentSenderUserId = -1; 79 80 private static final Handler sMainHandler = new Handler(Looper.getMainLooper()); 81 82 @Override onCreate(Bundle savedInstanceState)83 public void onCreate(Bundle savedInstanceState) { 84 super.onCreate(savedInstanceState); 85 86 final Activity activity = getActivity(); 87 mIsWorkProfile = activity.getIntent().getBooleanExtra( 88 UserUtils.EXTRA_IS_WORK_PROFILE, false); 89 mIsPrivateSpace = activity.getIntent().getBooleanExtra( 90 UserUtils.EXTRA_IS_PRIVATE_SPACE, false); 91 if (activity != null && activity.getIntent().getStringExtra(EXTRA_PACKAGE_NAME) != null) { 92 mCancelListener = 93 (d, w) -> { 94 activity.setResult(Activity.RESULT_CANCELED); 95 activity.finish(); 96 }; 97 // If mCancelListener is not null, fragment is started from 98 // ACTION_REQUEST_SET_AUTOFILL_SERVICE and we should always use the calling uid. 99 mIntentSenderUserId = UserHandle.myUserId(); 100 } 101 102 getUser(); 103 104 mSettingsPackageMonitor.register(activity, activity.getMainLooper(), false); 105 update(); 106 } 107 108 @Override newConfirmationDialogFragment( String selectedKey, CharSequence confirmationMessage)109 protected DefaultAppPickerFragment.ConfirmationDialogFragment newConfirmationDialogFragment( 110 String selectedKey, CharSequence confirmationMessage) { 111 final AutofillPickerConfirmationDialogFragment fragment = 112 new AutofillPickerConfirmationDialogFragment(); 113 fragment.init(this, selectedKey, confirmationMessage); 114 return fragment; 115 } 116 117 /** 118 * Custom dialog fragment that has a cancel listener used to propagate the result back to caller 119 * (for the cases where the picker is launched by {@code 120 * android.settings.REQUEST_SET_AUTOFILL_SERVICE}. 121 */ 122 public static class AutofillPickerConfirmationDialogFragment 123 extends DefaultAppPickerFragment.ConfirmationDialogFragment { 124 125 @Override onCreate(Bundle savedInstanceState)126 public void onCreate(Bundle savedInstanceState) { 127 final DefaultCombinedPicker target = (DefaultCombinedPicker) getTargetFragment(); 128 setCancelListener(target.mCancelListener); 129 super.onCreate(savedInstanceState); 130 } 131 132 @Override getPositiveButtonText()133 protected CharSequence getPositiveButtonText() { 134 final Bundle bundle = getArguments(); 135 if (TextUtils.isEmpty(bundle.getString(EXTRA_KEY))) { 136 return getContext() 137 .getString(R.string.credman_confirmation_turn_off_positive_button); 138 } 139 140 return getContext() 141 .getString(R.string.credman_confirmation_change_provider_positive_button); 142 } 143 } 144 145 @Override getPreferenceScreenResId()146 protected int getPreferenceScreenResId() { 147 return R.xml.default_credman_picker; 148 } 149 150 @Override getMetricsCategory()151 public int getMetricsCategory() { 152 return SettingsEnums.DEFAULT_AUTOFILL_PICKER; 153 } 154 155 @Override shouldShowItemNone()156 protected boolean shouldShowItemNone() { 157 return true; 158 } 159 160 /** Monitor coming and going auto fill services and calls {@link #update()} when necessary */ 161 private final PackageMonitor mSettingsPackageMonitor = 162 new PackageMonitor() { 163 @Override 164 public void onPackageAdded(String packageName, int uid) { 165 sMainHandler.post( 166 () -> { 167 // See b/296164461 for context 168 if (getContext() == null) { 169 Log.w(TAG, "context is null"); 170 return; 171 } 172 173 update(); 174 }); 175 } 176 177 @Override 178 public void onPackageModified(String packageName) { 179 sMainHandler.post( 180 () -> { 181 // See b/296164461 for context 182 if (getContext() == null) { 183 Log.w(TAG, "context is null"); 184 return; 185 } 186 187 update(); 188 }); 189 } 190 191 @Override 192 public void onPackageRemoved(String packageName, int uid) { 193 sMainHandler.post( 194 () -> { 195 // See b/296164461 for context 196 if (getContext() == null) { 197 Log.w(TAG, "context is null"); 198 return; 199 } 200 201 update(); 202 }); 203 } 204 }; 205 206 /** Update the data in this UI. */ update()207 private void update() { 208 updateCandidates(); 209 addAddServicePreference(); 210 } 211 212 @Override onDestroy()213 public void onDestroy() { 214 mSettingsPackageMonitor.unregister(); 215 super.onDestroy(); 216 } 217 218 /** 219 * Gets the preference that allows to add a new autofill service. 220 * 221 * @return The preference or {@code null} if no service can be added 222 */ newAddServicePreferenceOrNull()223 private Preference newAddServicePreferenceOrNull() { 224 final String searchUri = 225 Settings.Secure.getStringForUser( 226 getActivity().getContentResolver(), 227 Settings.Secure.AUTOFILL_SERVICE_SEARCH_URI, 228 getUser()); 229 if (TextUtils.isEmpty(searchUri)) { 230 return null; 231 } 232 233 final Intent addNewServiceIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)); 234 final Context context = getPrefContext(); 235 final Preference preference = new Preference(context); 236 preference.setOnPreferenceClickListener( 237 p -> { 238 context.startActivityAsUser(addNewServiceIntent, UserHandle.of(getUser())); 239 return true; 240 }); 241 preference.setTitle(R.string.print_menu_item_add_service); 242 preference.setIcon(R.drawable.ic_add_24dp); 243 preference.setOrder(Integer.MAX_VALUE - 1); 244 preference.setPersistent(false); 245 return preference; 246 } 247 248 /** 249 * Add a preference that allows the user to add a service if the market link for that is 250 * configured. 251 */ addAddServicePreference()252 private void addAddServicePreference() { 253 final Preference addNewServicePreference = newAddServicePreferenceOrNull(); 254 if (addNewServicePreference != null) { 255 getPreferenceScreen().addPreference(addNewServicePreference); 256 } 257 } 258 259 /** 260 * Get the Credential Manager service if we haven't already got it. We need to get the service 261 * later because if we do it in onCreate it will fail. 262 */ getCredentialProviderService()263 private @Nullable CredentialManager getCredentialProviderService() { 264 if (mCredentialManager == null) { 265 mCredentialManager = getContext().getSystemService(CredentialManager.class); 266 } 267 return mCredentialManager; 268 } 269 getAllProviders(int userId)270 private List<CombinedProviderInfo> getAllProviders(int userId) { 271 final Context context = getContext(); 272 final List<AutofillServiceInfo> autofillProviders = 273 AutofillServiceInfo.getAvailableServices(context, userId); 274 275 final CredentialManager service = getCredentialProviderService(); 276 final List<CredentialProviderInfo> credManProviders = new ArrayList<>(); 277 if (service != null) { 278 credManProviders.addAll( 279 service.getCredentialProviderServices( 280 userId, 281 CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_INCLUDING_HIDDEN)); 282 } 283 284 final String selectedAutofillProvider = 285 CredentialManagerPreferenceController 286 .getSelectedAutofillProvider(context, userId, TAG); 287 return CombinedProviderInfo.buildMergedList( 288 autofillProviders, credManProviders, selectedAutofillProvider); 289 } 290 291 getCandidates()292 protected List<DefaultAppInfo> getCandidates() { 293 final Context context = getContext(); 294 final int userId = getUser(); 295 final List<CombinedProviderInfo> allProviders = getAllProviders(userId); 296 final List<DefaultAppInfo> candidates = new ArrayList<>(); 297 298 for (CombinedProviderInfo cpi : allProviders) { 299 ServiceInfo brandingService = cpi.getBrandingService(); 300 ApplicationInfo appInfo = cpi.getApplicationInfo(); 301 302 if (brandingService != null) { 303 candidates.add( 304 new CredentialManagerDefaultAppInfo( 305 context, mPm, userId, brandingService, cpi)); 306 } else if (appInfo != null) { 307 candidates.add( 308 new CredentialManagerDefaultAppInfo(context, mPm, userId, appInfo, cpi)); 309 } 310 } 311 312 return candidates; 313 } 314 315 @Override bindPreferenceExtra( SelectorWithWidgetPreference pref, String key, CandidateInfo info, String defaultKey, String systemDefaultKey)316 public void bindPreferenceExtra( 317 SelectorWithWidgetPreference pref, 318 String key, 319 CandidateInfo info, 320 String defaultKey, 321 String systemDefaultKey) { 322 super.bindPreferenceExtra(pref, key, info, defaultKey, systemDefaultKey); 323 324 if (!(info instanceof CredentialManagerDefaultAppInfo)) { 325 Log.e(TAG, "Candidate info should be a subclass of CredentialManagerDefaultAppInfo"); 326 return; 327 } 328 329 if (!(pref instanceof RestrictedSelectorWithWidgetPreference)) { 330 Log.e(TAG, "Preference should be a subclass of RestrictedSelectorWithWidgetPreference"); 331 return; 332 } 333 334 CredentialManagerDefaultAppInfo credmanAppInfo = (CredentialManagerDefaultAppInfo) info; 335 RestrictedSelectorWithWidgetPreference rp = (RestrictedSelectorWithWidgetPreference) pref; 336 337 // Apply policy transparency. 338 rp.setDisabledByAdmin( 339 credmanAppInfo 340 .getCombinedProviderInfo() 341 .getDeviceAdminRestrictions(getContext(), getUser())); 342 } 343 344 @Override createPreference()345 protected SelectorWithWidgetPreference createPreference() { 346 return new RestrictedSelectorWithWidgetPreference(getPrefContext()); 347 } 348 349 /** This extends DefaultAppInfo with custom CredMan app info. */ 350 public static class CredentialManagerDefaultAppInfo extends DefaultAppInfo { 351 352 private final CombinedProviderInfo mCombinedProviderInfo; 353 CredentialManagerDefaultAppInfo( Context context, PackageManager pm, int uid, PackageItemInfo info, CombinedProviderInfo cpi)354 CredentialManagerDefaultAppInfo( 355 Context context, 356 PackageManager pm, 357 int uid, 358 PackageItemInfo info, 359 CombinedProviderInfo cpi) { 360 super(context, pm, uid, info, cpi.getSettingsSubtitle(), /* enabled= */ true); 361 mCombinedProviderInfo = cpi; 362 } 363 getCombinedProviderInfo()364 public @NonNull CombinedProviderInfo getCombinedProviderInfo() { 365 return mCombinedProviderInfo; 366 } 367 } 368 369 @Override getDefaultKey()370 protected String getDefaultKey() { 371 final int userId = getUser(); 372 final @Nullable CombinedProviderInfo topProvider = 373 CombinedProviderInfo.getTopProvider(getAllProviders(userId)); 374 375 if (topProvider != null) { 376 // Apply device admin restrictions to top provider. 377 if (topProvider.getDeviceAdminRestrictions(getContext(), userId) != null) { 378 return ""; 379 } 380 381 ApplicationInfo appInfo = topProvider.getApplicationInfo(); 382 if (appInfo != null) { 383 return appInfo.packageName; 384 } 385 } 386 387 return ""; 388 } 389 390 @Override getConfirmationMessage(CandidateInfo appInfo)391 protected CharSequence getConfirmationMessage(CandidateInfo appInfo) { 392 // If we are selecting none then show a warning label. 393 if (appInfo == null) { 394 final String message = 395 getContext() 396 .getString( 397 Flags.newSettingsUi() 398 ? R.string.credman_confirmation_message_new_ui 399 : R.string.credman_confirmation_message); 400 return Html.fromHtml(message); 401 } 402 final CharSequence appName = appInfo.loadLabel(); 403 final String message = 404 getContext() 405 .getString( 406 Flags.newSettingsUi() 407 ? R.string.credman_autofill_confirmation_message_new_ui 408 : R.string.credman_autofill_confirmation_message, 409 Html.escapeHtml(appName)); 410 return Html.fromHtml(message); 411 } 412 413 @Override setDefaultKey(String key)414 protected boolean setDefaultKey(String key) { 415 // Get the list of providers and see if any match the key (package name). 416 final List<CombinedProviderInfo> allProviders = getAllProviders(getUser()); 417 CombinedProviderInfo matchedProvider = null; 418 for (CombinedProviderInfo cpi : allProviders) { 419 if (cpi.getApplicationInfo().packageName.equals(key)) { 420 matchedProvider = cpi; 421 break; 422 } 423 } 424 425 // If there were none then clear the stored providers. 426 if (matchedProvider == null) { 427 setProviders(null, new ArrayList<>()); 428 return true; 429 } 430 431 // Get the component names and save them. 432 final List<String> credManComponents = new ArrayList<>(); 433 for (CredentialProviderInfo pi : matchedProvider.getCredentialProviderInfos()) { 434 credManComponents.add(pi.getServiceInfo().getComponentName().flattenToString()); 435 } 436 437 String autofillValue = null; 438 if (matchedProvider.getAutofillServiceInfo() != null) { 439 autofillValue = 440 matchedProvider 441 .getAutofillServiceInfo() 442 .getServiceInfo() 443 .getComponentName() 444 .flattenToString(); 445 } 446 447 setProviders(autofillValue, credManComponents); 448 449 // Check if activity was launched from Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE 450 // intent, and set proper result if so... 451 final Activity activity = getActivity(); 452 if (activity != null) { 453 final String packageName = activity.getIntent().getStringExtra(EXTRA_PACKAGE_NAME); 454 if (packageName != null) { 455 final int result = 456 key != null && key.startsWith(packageName) 457 ? Activity.RESULT_OK 458 : Activity.RESULT_CANCELED; 459 activity.setResult(result); 460 activity.finish(); 461 } 462 } 463 464 // TODO: Notify the rest 465 466 return true; 467 } 468 setProviders(String autofillProvider, List<String> primaryCredManProviders)469 private void setProviders(String autofillProvider, List<String> primaryCredManProviders) { 470 if (TextUtils.isEmpty(autofillProvider)) { 471 if (primaryCredManProviders.size() > 0) { 472 if (android.service.autofill.Flags.autofillCredmanDevIntegration()) { 473 autofillProvider = getCredentialAutofillService(getContext(), TAG); 474 } else { 475 autofillProvider = 476 CredentialManagerPreferenceController 477 .AUTOFILL_CREDMAN_ONLY_PROVIDER_PLACEHOLDER; 478 } 479 } 480 } 481 482 Settings.Secure.putStringForUser( 483 getContext().getContentResolver(), AUTOFILL_SETTING, autofillProvider, getUser()); 484 485 final CredentialManager service = getCredentialProviderService(); 486 if (service == null) { 487 return; 488 } 489 490 // Get the existing secondary providers since we don't touch them in 491 // this part of the UI we should just copy them over. 492 final List<String> credManProviders = new ArrayList<>(); 493 for (CredentialProviderInfo cpi : 494 service.getCredentialProviderServices( 495 getUser(), 496 CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_INCLUDING_HIDDEN)) { 497 498 if (cpi.isEnabled() && !cpi.isPrimary()) { 499 credManProviders.add(cpi.getServiceInfo().getComponentName().flattenToString()); 500 } 501 } 502 503 credManProviders.addAll(primaryCredManProviders); 504 505 // If there is no provider then clear all the providers. 506 if (TextUtils.isEmpty(autofillProvider) && primaryCredManProviders.isEmpty()) { 507 credManProviders.clear(); 508 } 509 510 service.setEnabledProviders( 511 primaryCredManProviders, 512 credManProviders, 513 getUser(), 514 ContextCompat.getMainExecutor(getContext()), 515 new OutcomeReceiver<Void, SetEnabledProvidersException>() { 516 @Override 517 public void onResult(Void result) { 518 Log.i(TAG, "setEnabledProviders success"); 519 } 520 521 @Override 522 public void onError(SetEnabledProvidersException e) { 523 Log.e(TAG, "setEnabledProviders error: " + e.toString()); 524 } 525 }); 526 } 527 getUser()528 protected int getUser() { 529 return UserUtils.getUser(mIsWorkProfile, mIsPrivateSpace, getContext()); 530 } 531 } 532