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.content.ActivityNotFoundException; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.ApplicationInfo; 24 import android.content.pm.ServiceInfo; 25 import android.credentials.CredentialProviderInfo; 26 import android.graphics.drawable.Drawable; 27 import android.os.UserHandle; 28 import android.service.autofill.AutofillServiceInfo; 29 import android.text.TextUtils; 30 import android.util.IconDrawableFactory; 31 import android.util.Log; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 36 import com.android.settingslib.RestrictedLockUtils; 37 import com.android.settingslib.RestrictedLockUtilsInternal; 38 39 import java.util.ArrayList; 40 import java.util.Collections; 41 import java.util.HashMap; 42 import java.util.HashSet; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Set; 46 47 /** 48 * Holds combined autofill and credential manager data grouped by package name. Contains backing 49 * logic for each row in settings. 50 */ 51 public final class CombinedProviderInfo { 52 private static final String TAG = "CombinedProviderInfo"; 53 private static final String SETTINGS_ACTIVITY_INTENT_ACTION = "android.intent.action.MAIN"; 54 private static final String SETTINGS_ACTIVITY_INTENT_CATEGORY = 55 "android.intent.category.DEFAULT"; 56 57 private final List<CredentialProviderInfo> mCredentialProviderInfos; 58 private final @Nullable AutofillServiceInfo mAutofillServiceInfo; 59 private final boolean mIsDefaultAutofillProvider; 60 private final boolean mIsPrimaryCredmanProvider; 61 62 /** Constructs an information instance from both autofill and credential provider. */ CombinedProviderInfo( @ullable List<CredentialProviderInfo> cpis, @Nullable AutofillServiceInfo asi, boolean isDefaultAutofillProvider, boolean isPrimaryCredmanProvider)63 public CombinedProviderInfo( 64 @Nullable List<CredentialProviderInfo> cpis, 65 @Nullable AutofillServiceInfo asi, 66 boolean isDefaultAutofillProvider, 67 boolean isPrimaryCredmanProvider) { 68 if (cpis == null) { 69 mCredentialProviderInfos = new ArrayList<>(); 70 } else { 71 mCredentialProviderInfos = new ArrayList<>(cpis); 72 } 73 mAutofillServiceInfo = asi; 74 mIsDefaultAutofillProvider = isDefaultAutofillProvider; 75 mIsPrimaryCredmanProvider = isPrimaryCredmanProvider; 76 } 77 78 /** Returns the credential provider info. */ 79 @NonNull getCredentialProviderInfos()80 public List<CredentialProviderInfo> getCredentialProviderInfos() { 81 return mCredentialProviderInfos; 82 } 83 84 /** Returns the autofill provider info. */ 85 @Nullable getAutofillServiceInfo()86 public AutofillServiceInfo getAutofillServiceInfo() { 87 return mAutofillServiceInfo; 88 } 89 90 /** Returns the application info. */ getApplicationInfo()91 public @Nullable ApplicationInfo getApplicationInfo() { 92 if (!mCredentialProviderInfos.isEmpty()) { 93 return mCredentialProviderInfos.get(0).getServiceInfo().applicationInfo; 94 } 95 return mAutofillServiceInfo.getServiceInfo().applicationInfo; 96 } 97 98 /** Returns the package name. */ getPackageName()99 public @Nullable String getPackageName() { 100 ApplicationInfo ai = getApplicationInfo(); 101 if (ai != null) { 102 return ai.packageName; 103 } 104 105 return null; 106 } 107 108 /** Returns the settings activity. */ getSettingsActivity()109 public @Nullable String getSettingsActivity() { 110 // This logic is not used by the top entry but rather what activity should 111 // be launched from the settings screen. 112 for (CredentialProviderInfo cpi : mCredentialProviderInfos) { 113 final CharSequence settingsActivity = cpi.getSettingsActivity(); 114 if (!TextUtils.isEmpty(settingsActivity)) { 115 return String.valueOf(settingsActivity); 116 } 117 } 118 119 if (mAutofillServiceInfo != null) { 120 final String settingsActivity = mAutofillServiceInfo.getSettingsActivity(); 121 if (!TextUtils.isEmpty(settingsActivity)) { 122 return settingsActivity; 123 } 124 } 125 126 return null; 127 } 128 129 /** Returns the app icon. */ 130 @Nullable getAppIcon(@onNull Context context, int userId)131 public Drawable getAppIcon(@NonNull Context context, int userId) { 132 final IconDrawableFactory factory = IconDrawableFactory.newInstance(context); 133 final ServiceInfo brandingService = getBrandingService(); 134 final ApplicationInfo appInfo = getApplicationInfo(); 135 136 Drawable icon = null; 137 if (brandingService != null && appInfo != null) { 138 icon = factory.getBadgedIcon(brandingService, appInfo, userId); 139 } 140 141 // If the branding service gave us a icon then use that. 142 if (icon != null) { 143 return icon; 144 } 145 146 // Otherwise fallback to the app icon and then the package name. 147 if (appInfo != null) { 148 return factory.getBadgedIcon(appInfo, userId); 149 } 150 return null; 151 } 152 153 /** Returns the app name. */ 154 @Nullable getAppName(@onNull Context context)155 public CharSequence getAppName(@NonNull Context context) { 156 CharSequence name = ""; 157 ServiceInfo brandingService = getBrandingService(); 158 if (brandingService != null) { 159 name = brandingService.loadLabel(context.getPackageManager()); 160 } 161 162 // If the branding service gave us a name then use that. 163 if (!TextUtils.isEmpty(name)) { 164 return name; 165 } 166 167 // Otherwise fallback to the app label and then the package name. 168 final ApplicationInfo appInfo = getApplicationInfo(); 169 if (appInfo != null) { 170 name = appInfo.loadLabel(context.getPackageManager()); 171 if (TextUtils.isEmpty(name)) { 172 return appInfo.packageName; 173 } 174 } 175 return ""; 176 } 177 178 /** Gets the service to use for branding (name, icons). */ getBrandingService()179 public @Nullable ServiceInfo getBrandingService() { 180 // If the app has an autofill service then use that. 181 if (mAutofillServiceInfo != null) { 182 return mAutofillServiceInfo.getServiceInfo(); 183 } 184 185 // If there are no credman providers then stop here. 186 if (mCredentialProviderInfos.isEmpty()) { 187 return null; 188 } 189 190 // Build a list of credential providers and sort them by component names 191 // alphabetically to ensure we are deterministic when picking the provider. 192 Map<String, ServiceInfo> flattenedNamesToServices = new HashMap<>(); 193 List<String> flattenedNames = new ArrayList<>(); 194 for (CredentialProviderInfo cpi : mCredentialProviderInfos) { 195 final String flattenedName = cpi.getComponentName().flattenToString(); 196 flattenedNamesToServices.put(flattenedName, cpi.getServiceInfo()); 197 flattenedNames.add(flattenedName); 198 } 199 200 Collections.sort(flattenedNames); 201 return flattenedNamesToServices.get(flattenedNames.get(0)); 202 } 203 204 /** Returns whether the provider is the default autofill provider. */ isDefaultAutofillProvider()205 public boolean isDefaultAutofillProvider() { 206 return mIsDefaultAutofillProvider; 207 } 208 209 /** Returns whether the provider is the default credman provider. */ isPrimaryCredmanProvider()210 public boolean isPrimaryCredmanProvider() { 211 return mIsPrimaryCredmanProvider; 212 } 213 214 /** Returns the settings subtitle. */ 215 @Nullable getSettingsSubtitle()216 public String getSettingsSubtitle() { 217 List<String> subtitles = new ArrayList<>(); 218 for (CredentialProviderInfo cpi : mCredentialProviderInfos) { 219 // Convert from a CharSequence. 220 String subtitle = String.valueOf(cpi.getSettingsSubtitle()); 221 if (subtitle != null && !TextUtils.isEmpty(subtitle) && !subtitle.equals("null")) { 222 subtitles.add(subtitle); 223 } 224 } 225 226 if (subtitles.size() == 0) { 227 return ""; 228 } 229 230 return String.join(", ", subtitles); 231 } 232 233 /** Returns the autofill component name string. */ 234 @Nullable getAutofillServiceString()235 public String getAutofillServiceString() { 236 if (mAutofillServiceInfo != null) { 237 return mAutofillServiceInfo.getServiceInfo().getComponentName().flattenToString(); 238 } 239 return null; 240 } 241 242 /** Returns whether this entry contains a system provider. */ isCredentialManagerSystemProvider()243 public boolean isCredentialManagerSystemProvider() { 244 for (CredentialProviderInfo cpi : mCredentialProviderInfos) { 245 if (cpi.isSystemProvider()) { 246 return true; 247 } 248 } 249 250 return false; 251 } 252 253 /** Returns whether this entry has device admin restrictions. */ 254 @Nullable getDeviceAdminRestrictions( Context context, int userId)255 public RestrictedLockUtils.EnforcedAdmin getDeviceAdminRestrictions( 256 Context context, int userId) { 257 final String packageName = getPackageName(); 258 if (TextUtils.isEmpty(packageName)) { 259 return null; 260 } 261 262 return RestrictedLockUtilsInternal.checkIfApplicationCanBeCredentialManagerProvider( 263 context.createContextAsUser(UserHandle.of(userId), /* flags= */ 0), 264 packageName); 265 } 266 267 /** Returns the provider that gets the top spot. */ getTopProvider( List<CombinedProviderInfo> providers)268 public static @Nullable CombinedProviderInfo getTopProvider( 269 List<CombinedProviderInfo> providers) { 270 // If there is an autofill provider then it should be the 271 // top app provider. 272 for (CombinedProviderInfo cpi : providers) { 273 if (cpi.isDefaultAutofillProvider()) { 274 return cpi; 275 } 276 } 277 278 // If there is a primary cred man provider then return that. 279 for (CombinedProviderInfo cpi : providers) { 280 if (cpi.isPrimaryCredmanProvider()) { 281 return cpi; 282 } 283 } 284 285 return null; 286 } 287 buildMergedList( List<AutofillServiceInfo> asiList, List<CredentialProviderInfo> cpiList, @Nullable String defaultAutofillProvider)288 public static List<CombinedProviderInfo> buildMergedList( 289 List<AutofillServiceInfo> asiList, 290 List<CredentialProviderInfo> cpiList, 291 @Nullable String defaultAutofillProvider) { 292 ComponentName defaultAutofillProviderComponent = 293 (defaultAutofillProvider == null) 294 ? null 295 : ComponentName.unflattenFromString(defaultAutofillProvider); 296 297 // Index the autofill providers by package name. 298 Set<String> packageNames = new HashSet<>(); 299 Map<String, List<AutofillServiceInfo>> autofillServices = new HashMap<>(); 300 for (AutofillServiceInfo asi : asiList) { 301 final String packageName = asi.getServiceInfo().packageName; 302 if (!autofillServices.containsKey(packageName)) { 303 autofillServices.put(packageName, new ArrayList<>()); 304 } 305 306 autofillServices.get(packageName).add(asi); 307 packageNames.add(packageName); 308 } 309 310 // Index the credman providers by package name. 311 Map<String, List<CredentialProviderInfo>> credmanServices = new HashMap<>(); 312 for (CredentialProviderInfo cpi : cpiList) { 313 String packageName = cpi.getServiceInfo().packageName; 314 if (!credmanServices.containsKey(packageName)) { 315 credmanServices.put(packageName, new ArrayList<>()); 316 } 317 318 credmanServices.get(packageName).add(cpi); 319 packageNames.add(packageName); 320 } 321 322 // Now go through and build the joint datasets. 323 List<CombinedProviderInfo> cmpi = new ArrayList<>(); 324 for (String packageName : packageNames) { 325 List<AutofillServiceInfo> asi = 326 autofillServices.getOrDefault(packageName, new ArrayList<>()); 327 List<CredentialProviderInfo> cpi = 328 credmanServices.getOrDefault(packageName, new ArrayList<>()); 329 330 // If there are multiple autofill services then pick the first one. 331 AutofillServiceInfo selectedAsi = null; 332 if (asi != null && !asi.isEmpty()) { 333 selectedAsi = asi.get(0); 334 } 335 336 // Check if we are the default autofill provider. 337 boolean isDefaultAutofillProvider = false; 338 if (defaultAutofillProviderComponent != null 339 && defaultAutofillProviderComponent.getPackageName().equals(packageName)) { 340 isDefaultAutofillProvider = true; 341 } 342 343 // Check if we have any enabled cred man services. 344 boolean isPrimaryCredmanProvider = false; 345 if (cpi != null && !cpi.isEmpty()) { 346 isPrimaryCredmanProvider = cpi.get(0).isPrimary(); 347 } 348 349 cmpi.add( 350 new CombinedProviderInfo( 351 cpi, selectedAsi, isDefaultAutofillProvider, isPrimaryCredmanProvider)); 352 } 353 354 return cmpi; 355 } 356 createSettingsActivityIntent( @ullable CharSequence packageName, @Nullable CharSequence settingsActivity)357 public static @Nullable Intent createSettingsActivityIntent( 358 @Nullable CharSequence packageName, @Nullable CharSequence settingsActivity) { 359 if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(settingsActivity)) { 360 return null; 361 } 362 363 ComponentName cn = 364 new ComponentName(String.valueOf(packageName), String.valueOf(settingsActivity)); 365 if (cn == null) { 366 Log.e( 367 TAG, 368 "Failed to deserialize settingsActivity attribute, we got: " 369 + String.valueOf(packageName) 370 + " and " 371 + String.valueOf(settingsActivity)); 372 return null; 373 } 374 375 Intent intent = new Intent(SETTINGS_ACTIVITY_INTENT_ACTION); 376 intent.addCategory(SETTINGS_ACTIVITY_INTENT_CATEGORY); 377 intent.setComponent(cn); 378 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 379 return intent; 380 } 381 382 /** Launches the settings activity intent. */ launchSettingsActivityIntent( @onNull Context context, @Nullable CharSequence packageName, @Nullable CharSequence settingsActivity, int userId)383 public static void launchSettingsActivityIntent( 384 @NonNull Context context, 385 @Nullable CharSequence packageName, 386 @Nullable CharSequence settingsActivity, 387 int userId) { 388 Intent settingsIntent = createSettingsActivityIntent(packageName, settingsActivity); 389 if (settingsIntent == null) { 390 return; 391 } 392 393 try { 394 context.startActivityAsUser(settingsIntent, UserHandle.of(userId)); 395 } catch (ActivityNotFoundException e) { 396 Log.e(TAG, "Failed to open settings activity", e); 397 } 398 } 399 } 400