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