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