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