1 /*
2  * Copyright (C) 2022 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.server.pm;
18 
19 import static android.content.pm.PackageManager.MATCH_ALL;
20 
21 import static com.android.server.pm.PackageManagerService.DEBUG_DOMAIN_VERIFICATION;
22 import static com.android.server.pm.PackageManagerService.DEBUG_PREFERRED;
23 import static com.android.server.pm.PackageManagerService.TAG;
24 
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.annotation.UserIdInt;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.pm.PackageManager;
31 import android.content.pm.ResolveInfo;
32 import android.content.pm.UserInfo;
33 import android.content.pm.UserProperties;
34 import android.os.Process;
35 import android.text.TextUtils;
36 import android.util.Pair;
37 import android.util.Slog;
38 import android.util.SparseArray;
39 
40 import com.android.internal.config.appcloning.AppCloningDeviceConfigHelper;
41 import com.android.server.LocalServices;
42 import com.android.server.pm.pkg.PackageStateInternal;
43 import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
44 import com.android.server.pm.verify.domain.DomainVerificationUtils;
45 
46 import java.util.ArrayList;
47 import java.util.HashSet;
48 import java.util.List;
49 import java.util.Set;
50 import java.util.function.Function;
51 
52 /**
53  * Rule based engine which decides strategy to be used for source,target pair and does cross profile
54  * intent resolution. Currently, we have only default and clone strategy. The major known use-case
55  * for default is work profile.
56  */
57 public class CrossProfileIntentResolverEngine {
58 
59     private final UserManagerService mUserManager;
60     private final DomainVerificationManagerInternal mDomainVerificationManager;
61     private final DefaultAppProvider mDefaultAppProvider;
62     private final Context mContext;
63     private final UserManagerInternal mUserManagerInternal;
64 
65     private AppCloningDeviceConfigHelper mAppCloningDeviceConfigHelper;
66 
CrossProfileIntentResolverEngine(UserManagerService userManager, DomainVerificationManagerInternal domainVerificationManager, DefaultAppProvider defaultAppProvider, Context context)67     public CrossProfileIntentResolverEngine(UserManagerService userManager,
68             DomainVerificationManagerInternal domainVerificationManager,
69             DefaultAppProvider defaultAppProvider, Context context) {
70         mUserManager = userManager;
71         mDomainVerificationManager = domainVerificationManager;
72         mDefaultAppProvider = defaultAppProvider;
73         mContext = context;
74         mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
75     }
76 
77     /**
78      * Returns the list of {@link CrossProfileDomainInfo} which contains {@link ResolveInfo} from
79      * profiles linked directly/indirectly to user. Work-Owner as well as Clone-Owner
80      * are directly related as they are child of owner. Work-Clone are indirectly linked through
81      * owner profile.
82      * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi}
83      * @param intent request
84      * @param resolvedType the MIME data type of intent request
85      * @param userId source user for which intent request is called
86      * @param flags used for intent resolution
87      * @param pkgName the application package name this Intent is limited to.
88      * @param hasNonNegativePriorityResult signifies if current profile have any non-negative(active
89      *                                     and valid) ResolveInfo in current profile.
90      * @param resolveForStart true if resolution occurs to start an activity.
91      * @param pkgSettingFunction function to find PackageStateInternal for given package
92      * @return list of {@link CrossProfileDomainInfo} from linked profiles.
93      */
resolveIntent(@onNull Computer computer, Intent intent, String resolvedType, int userId, long flags, String pkgName, boolean hasNonNegativePriorityResult, boolean resolveForStart, Function<String, PackageStateInternal> pkgSettingFunction)94     public List<CrossProfileDomainInfo> resolveIntent(@NonNull Computer computer, Intent intent,
95             String resolvedType, int userId, long flags, String pkgName,
96             boolean hasNonNegativePriorityResult, boolean resolveForStart,
97             Function<String, PackageStateInternal> pkgSettingFunction) {
98         return resolveIntentInternal(computer, intent, resolvedType, userId, userId, flags, pkgName,
99                 hasNonNegativePriorityResult, resolveForStart, pkgSettingFunction, null);
100     }
101 
102     /**
103      * Resolves intent in directly linked profiles and return list of {@link CrossProfileDomainInfo}
104      * which contains {@link ResolveInfo}. This would also recursively call profiles not directly
105      * linked using Depth First Search.
106      *
107      * It first finds {@link CrossProfileIntentFilter} configured in current profile to find list of
108      * target user profiles that can serve current intent request. It uses corresponding strategy
109      * for each pair (source,target) user to resolve intent from target profile and returns combined
110      * results.
111      * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi}
112      * @param intent request
113      * @param resolvedType the MIME data type of intent request
114      * @param sourceUserId source user for which intent request is called
115      * @param userId current user for cross profile resolution
116      * @param flags used for intent resolution
117      * @param pkgName the application package name this Intent is limited to.
118      * @param hasNonNegativePriorityResult signifies if current profile have any non-negative(active
119      *                                     and valid) ResolveInfo in current profile.
120      * @param resolveForStart true if resolution occurs to start an activity.
121      * @param pkgSettingFunction function to find PackageStateInternal for given package
122      * @param visitedUserIds users for which we have already performed resolution
123      * @return list of {@link CrossProfileDomainInfo} from linked profiles.
124      */
resolveIntentInternal(@onNull Computer computer, Intent intent, String resolvedType, int sourceUserId, int userId, long flags, String pkgName, boolean hasNonNegativePriorityResult, boolean resolveForStart, Function<String, PackageStateInternal> pkgSettingFunction, Set<Integer> visitedUserIds)125     private List<CrossProfileDomainInfo> resolveIntentInternal(@NonNull Computer computer,
126             Intent intent, String resolvedType, int sourceUserId, int userId, long flags,
127             String pkgName, boolean hasNonNegativePriorityResult, boolean resolveForStart,
128             Function<String, PackageStateInternal> pkgSettingFunction,
129             Set<Integer> visitedUserIds) {
130 
131         if (visitedUserIds != null) visitedUserIds.add(userId);
132         List<CrossProfileDomainInfo> crossProfileDomainInfos = new ArrayList<>();
133 
134         List<CrossProfileIntentFilter> matchingFilters =
135                 computer.getMatchingCrossProfileIntentFilters(intent, resolvedType,
136                         userId);
137 
138         if (matchingFilters == null || matchingFilters.isEmpty()) {
139             /** if intent is web intent, checking if parent profile should handle the intent
140              * even if there is no matching filter. The configuration is based on user profile
141              * restriction android.os.UserManager#ALLOW_PARENT_PROFILE_APP_LINKING **/
142             if (sourceUserId == userId && intent.hasWebURI()) {
143                 UserInfo parent = computer.getProfileParent(userId);
144                 if (parent != null) {
145                     CrossProfileDomainInfo generalizedCrossProfileDomainInfo = computer
146                             .getCrossProfileDomainPreferredLpr(intent, resolvedType, flags,
147                                     userId, parent.id);
148                     if (generalizedCrossProfileDomainInfo != null) {
149                         crossProfileDomainInfos.add(generalizedCrossProfileDomainInfo);
150                     }
151                 }
152             }
153             return crossProfileDomainInfos;
154         }
155 
156         // Grouping the CrossProfileIntentFilters based on targerId
157         SparseArray<List<CrossProfileIntentFilter>> crossProfileIntentFiltersByUser =
158                 new SparseArray<>();
159 
160         for (int index = 0; index < matchingFilters.size(); index++) {
161             CrossProfileIntentFilter crossProfileIntentFilter = matchingFilters.get(index);
162 
163             if (!crossProfileIntentFiltersByUser
164                     .contains(crossProfileIntentFilter.mTargetUserId)) {
165                 crossProfileIntentFiltersByUser.put(crossProfileIntentFilter.mTargetUserId,
166                         new ArrayList<>());
167             }
168             crossProfileIntentFiltersByUser.get(crossProfileIntentFilter.mTargetUserId)
169                     .add(crossProfileIntentFilter);
170         }
171 
172         if (visitedUserIds == null) {
173             visitedUserIds = new HashSet<>();
174             visitedUserIds.add(userId);
175         }
176 
177         /*
178          For each target user, we would call their corresponding strategy
179          {@link CrossProfileResolver} to resolve intent in corresponding user
180          */
181         for (int index = 0; index < crossProfileIntentFiltersByUser.size(); index++) {
182 
183             int targetUserId = crossProfileIntentFiltersByUser.keyAt(index);
184 
185             //if user is already visited then skip resolution for particular user.
186             if (visitedUserIds.contains(targetUserId)) {
187                 continue;
188             }
189 
190             // Choosing strategy based on source and target user
191             CrossProfileResolver crossProfileResolver =
192                     chooseCrossProfileResolver(computer, userId, targetUserId,
193                             resolveForStart, flags);
194 
195         /*
196         If {@link CrossProfileResolver} is available for source,target pair we will call it to
197         get {@link CrossProfileDomainInfo}s from that user.
198          */
199             if (crossProfileResolver != null) {
200                 List<CrossProfileDomainInfo> crossProfileInfos = crossProfileResolver
201                         .resolveIntent(computer, intent, resolvedType, userId,
202                                 targetUserId, flags, pkgName,
203                                 crossProfileIntentFiltersByUser.valueAt(index),
204                                 hasNonNegativePriorityResult, pkgSettingFunction);
205                 crossProfileDomainInfos.addAll(crossProfileInfos);
206                 visitedUserIds.add(targetUserId);
207 
208                 /*
209                 Adding target user to queue if flag
210                 {@link CrossProfileIntentFilter#FLAG_ALLOW_CHAINED_RESOLUTION} is set for any
211                 {@link CrossProfileIntentFilter}
212                  */
213                 boolean allowChainedResolution = false;
214                 for (int filterIndex = 0; filterIndex < crossProfileIntentFiltersByUser
215                         .valueAt(index).size(); filterIndex++) {
216                     if ((CrossProfileIntentFilter
217                             .FLAG_ALLOW_CHAINED_RESOLUTION & crossProfileIntentFiltersByUser
218                             .valueAt(index).get(filterIndex).mFlags) != 0) {
219                         allowChainedResolution = true;
220                         break;
221                     }
222                 }
223                 if (allowChainedResolution) {
224                     crossProfileDomainInfos.addAll(resolveIntentInternal(computer, intent,
225                             resolvedType, sourceUserId, targetUserId, flags, pkgName,
226                             hasNonNegativePriority(crossProfileInfos), resolveForStart,
227                             pkgSettingFunction, visitedUserIds));
228                 }
229 
230             }
231         }
232 
233         return crossProfileDomainInfos;
234     }
235 
236 
237     /**
238      * Returns {@link CrossProfileResolver} strategy based on source and target user
239      * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi}
240      * @param sourceUserId source user
241      * @param targetUserId target user
242      * @param resolveForStart true if resolution occurs to start an activity.
243      * @param flags used for intent resolver selection
244      * @return {@code CrossProfileResolver} which has value if source and target have
245      * strategy configured otherwise null.
246      */
247     @SuppressWarnings("unused")
chooseCrossProfileResolver(@onNull Computer computer, @UserIdInt int sourceUserId, @UserIdInt int targetUserId, boolean resolveForStart, long flags)248     private CrossProfileResolver chooseCrossProfileResolver(@NonNull Computer computer,
249             @UserIdInt int sourceUserId, @UserIdInt int targetUserId, boolean resolveForStart,
250             long flags) {
251         /**
252          * If source or target user is clone profile, using {@link NoFilteringResolver}
253          * We would return NoFilteringResolver only if it is allowed(feature flag is set).
254          */
255         if (shouldUseNoFilteringResolver(sourceUserId, targetUserId)) {
256             if (mAppCloningDeviceConfigHelper == null) {
257                 //lazy initialization of helper till required, to improve performance.
258                 mAppCloningDeviceConfigHelper = AppCloningDeviceConfigHelper.getInstance(mContext);
259             }
260             if (NoFilteringResolver.isIntentRedirectionAllowed(mContext,
261                     mAppCloningDeviceConfigHelper, resolveForStart, flags)) {
262                 return new NoFilteringResolver(computer.getComponentResolver(),
263                         mUserManager);
264             } else {
265                 return null;
266             }
267         }
268         return new DefaultCrossProfileResolver(computer.getComponentResolver(),
269                 mUserManager, mDomainVerificationManager);
270     }
271 
272     /**
273      * Returns true if we source user can reach target user for given intent. The source can
274      * directly or indirectly reach to target.
275      * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi}
276      * @param intent request
277      * @param resolvedType the MIME data type of intent request
278      * @param sourceUserId source user
279      * @param targetUserId target user
280      * @return true if we source user can reach target user for given intent
281      */
canReachTo(@onNull Computer computer, @NonNull Intent intent, @Nullable String resolvedType, @UserIdInt int sourceUserId, @UserIdInt int targetUserId)282     public boolean canReachTo(@NonNull Computer computer, @NonNull Intent intent,
283             @Nullable String resolvedType, @UserIdInt int sourceUserId,
284             @UserIdInt int targetUserId) {
285         Set<Integer> visitedUserIds = new HashSet<>();
286         return canReachToInternal(computer, intent, resolvedType, sourceUserId, targetUserId,
287                 visitedUserIds);
288     }
289 
290     /**
291      * Returns true if we source user can reach target user for given intent. The source can
292      * directly or indirectly reach to target. This will perform depth first search to check if
293      * source can reach target.
294      * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi}
295      * @param intent request
296      * @param resolvedType the MIME data type of intent request
297      * @param sourceUserId source user
298      * @param targetUserId target user
299      * @param visitedUserIds users for which resolution is checked
300      * @return true if we source user can reach target user for given intent
301      */
canReachToInternal(@onNull Computer computer, @NonNull Intent intent, @Nullable String resolvedType, @UserIdInt int sourceUserId, @UserIdInt int targetUserId, Set<Integer> visitedUserIds)302     private boolean canReachToInternal(@NonNull Computer computer, @NonNull Intent intent,
303             @Nullable String resolvedType, @UserIdInt int sourceUserId,
304             @UserIdInt int targetUserId, Set<Integer> visitedUserIds) {
305         if (sourceUserId == targetUserId) return true;
306         visitedUserIds.add(sourceUserId);
307 
308         List<CrossProfileIntentFilter> matches =
309                 computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, sourceUserId);
310 
311         if (matches != null) {
312             for (int index = 0; index < matches.size(); index++) {
313                 CrossProfileIntentFilter crossProfileIntentFilter = matches.get(index);
314                 if (crossProfileIntentFilter.mTargetUserId == targetUserId) {
315                     return true;
316                 }
317                 if (visitedUserIds.contains(crossProfileIntentFilter.mTargetUserId)) {
318                     continue;
319                 }
320 
321                 /*
322                  If source cannot directly reach to target, we will add
323                  CrossProfileIntentFilter.mTargetUserId user to queue to check if target user
324                  can be reached via CrossProfileIntentFilter.mTargetUserId i.e. it can be
325                  indirectly reached through chained/linked profiles.
326                  */
327                 if ((CrossProfileIntentFilter.FLAG_ALLOW_CHAINED_RESOLUTION
328                         & crossProfileIntentFilter.mFlags) != 0) {
329                     visitedUserIds.add(crossProfileIntentFilter.mTargetUserId);
330                     if (canReachToInternal(computer, intent, resolvedType,
331                             crossProfileIntentFilter.mTargetUserId, targetUserId, visitedUserIds)) {
332                         return true;
333                     }
334                 }
335             }
336         }
337         return false;
338     }
339 
340     /**
341      * Checks if any of the matching {@link CrossProfileIntentFilter} suggest we should skip the
342      * current profile based on flag {@link PackageManager#SKIP_CURRENT_PROFILE}.
343      * @param computer {@link Computer} instance used to find {@link CrossProfileIntentFilter}
344      *                                 for user
345      * @param intent request
346      * @param resolvedType the MIME data type of intent request
347      * @param sourceUserId id of initiating user space
348      * @return boolean if we should skip resolution in current/source profile.
349      */
shouldSkipCurrentProfile(Computer computer, Intent intent, String resolvedType, int sourceUserId)350     public boolean shouldSkipCurrentProfile(Computer computer, Intent intent, String resolvedType,
351             int sourceUserId) {
352         List<CrossProfileIntentFilter> matches =
353                 computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, sourceUserId);
354         if (matches != null) {
355             for (int matchIndex = 0; matchIndex < matches.size(); matchIndex++) {
356                 CrossProfileIntentFilter crossProfileIntentFilter = matches.get(matchIndex);
357                 if ((crossProfileIntentFilter.getFlags()
358                         & PackageManager.SKIP_CURRENT_PROFILE) != 0) {
359                     return true;
360                 }
361             }
362         }
363         return false;
364     }
365 
366     /**
367      * Combines result from current and cross profile. This also does filtering based on domain(if
368      * required).
369      * @param computer {@link Computer} instance
370      * @param intent request
371      * @param resolvedType the MIME data type of intent request
372      * @param instantAppPkgName package name if instant app is allowed
373      * @param pkgName the application package name this Intent is limited to.
374      * @param allowDynamicSplits true if dynamic splits is allowed
375      * @param matchFlags flags for intent request
376      * @param userId user id of source user
377      * @param filterCallingUid uid of calling process
378      * @param resolveForStart true if resolution occurs because an application is starting
379      * @param candidates resolveInfos from current profile
380      * @param crossProfileCandidates crossProfileDomainInfos from cross profile, it has ResolveInfo
381      * @param areWebInstantAppsDisabled true if web instant apps are disabled
382      * @param addInstant true if instant apps are allowed
383      * @param sortResult true if caller would need to sort the results
384      * @param pkgSettingFunction function to find PackageStateInternal for given package
385      * @return QueryIntentActivitiesResult which contains resolveInfos
386      */
combineFilterAndCreateQueryActivitiesResponse( Computer computer, Intent intent, String resolvedType, String instantAppPkgName, String pkgName, boolean allowDynamicSplits, long matchFlags, int userId, int filterCallingUid, boolean resolveForStart, List<ResolveInfo> candidates, List<CrossProfileDomainInfo> crossProfileCandidates, boolean areWebInstantAppsDisabled, boolean addInstant, boolean sortResult, Function<String, PackageStateInternal> pkgSettingFunction)387     public QueryIntentActivitiesResult combineFilterAndCreateQueryActivitiesResponse(
388             Computer computer, Intent intent, String resolvedType, String instantAppPkgName,
389             String pkgName, boolean allowDynamicSplits, long matchFlags, int userId,
390             int filterCallingUid, boolean resolveForStart, List<ResolveInfo> candidates,
391             List<CrossProfileDomainInfo> crossProfileCandidates, boolean areWebInstantAppsDisabled,
392             boolean addInstant, boolean sortResult,
393             Function<String, PackageStateInternal> pkgSettingFunction) {
394 
395         if (shouldSkipCurrentProfile(computer, intent, resolvedType, userId)) {
396             /*
397              if current profile is skipped return results from cross profile after filtering
398              ephemeral activities.
399              */
400             candidates = resolveInfoFromCrossProfileDomainInfo(crossProfileCandidates);
401             return new QueryIntentActivitiesResult(computer.applyPostResolutionFilter(candidates,
402                     instantAppPkgName, allowDynamicSplits, filterCallingUid, resolveForStart,
403                     userId, intent));
404         }
405 
406         if (pkgName == null && intent.hasWebURI()) {
407             if (!addInstant && ((candidates.size() <= 1 && crossProfileCandidates.isEmpty())
408                     || (candidates.isEmpty() && !crossProfileCandidates.isEmpty()))) {
409                 candidates.addAll(resolveInfoFromCrossProfileDomainInfo(crossProfileCandidates));
410                 return new QueryIntentActivitiesResult(computer.applyPostResolutionFilter(
411                         candidates, instantAppPkgName, allowDynamicSplits, filterCallingUid,
412                         resolveForStart, userId, intent));
413             }
414             /*
415              if there are multiple results from current and cross profile, combining and filtering
416              results based on domain priority.
417              */
418             candidates = filterCandidatesWithDomainPreferredActivitiesLPr(computer, intent,
419                     matchFlags, candidates, crossProfileCandidates, userId,
420                     areWebInstantAppsDisabled, resolveForStart, pkgSettingFunction);
421         } else {
422             candidates.addAll(resolveInfoFromCrossProfileDomainInfo(crossProfileCandidates));
423         }
424         return new QueryIntentActivitiesResult(sortResult, addInstant, candidates);
425     }
426 
427     /**
428      * It filters and combines results from current and cross profile based on domain priority.
429      * @param computer {@link Computer} instance
430      * @param intent request
431      * @param matchFlags flags for intent request
432      * @param candidates resolveInfos from current profile
433      * @param crossProfileCandidates crossProfileDomainInfos from cross profile, it have ResolveInfo
434      * @param userId user id of source user
435      * @param areWebInstantAppsDisabled true if web instant apps are disabled
436      * @param resolveForStart true if intent is for resolution
437      * @param pkgSettingFunction function to find PackageStateInternal for given package
438      * @return list of ResolveInfo
439      */
filterCandidatesWithDomainPreferredActivitiesLPr(Computer computer, Intent intent, long matchFlags, List<ResolveInfo> candidates, List<CrossProfileDomainInfo> crossProfileCandidates, int userId, boolean areWebInstantAppsDisabled, boolean resolveForStart, Function<String, PackageStateInternal> pkgSettingFunction)440     private List<ResolveInfo> filterCandidatesWithDomainPreferredActivitiesLPr(Computer computer,
441             Intent intent, long matchFlags, List<ResolveInfo> candidates,
442             List<CrossProfileDomainInfo> crossProfileCandidates, int userId,
443             boolean areWebInstantAppsDisabled, boolean resolveForStart,
444             Function<String, PackageStateInternal> pkgSettingFunction) {
445         final boolean debug = (intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0;
446 
447         if (DEBUG_PREFERRED || DEBUG_DOMAIN_VERIFICATION) {
448             Slog.v(TAG, "Filtering results with preferred activities. Candidates count: "
449                     + candidates.size());
450         }
451 
452         final List<ResolveInfo> result =
453                 filterCandidatesWithDomainPreferredActivitiesLPrBody(computer, intent, matchFlags,
454                         candidates, crossProfileCandidates, userId, areWebInstantAppsDisabled,
455                         debug, resolveForStart, pkgSettingFunction);
456 
457         if (DEBUG_PREFERRED || DEBUG_DOMAIN_VERIFICATION) {
458             Slog.v(TAG, "Filtered results with preferred activities. New candidates count: "
459                     + result.size());
460             for (ResolveInfo info : result) {
461                 Slog.v(TAG, " + " + info.activityInfo);
462             }
463         }
464         return result;
465     }
466 
467     /**
468      * Filters candidates satisfying domain criteria.
469      * @param computer {@link Computer} instance
470      * @param intent request
471      * @param matchFlags flags for intent request
472      * @param candidates resolveInfos from current profile
473      * @param crossProfileCandidates crossProfileDomainInfos from cross profile, it have ResolveInfo
474      * @param userId user id of source user
475      * @param areWebInstantAppsDisabled true if web instant apps are disabled
476      * @param debug true if resolution logs needed to be printed
477      * @param resolveForStart true if intent is for resolution
478      * @param pkgSettingFunction function to find PackageStateInternal for given package
479      * @return list of resolve infos
480      */
filterCandidatesWithDomainPreferredActivitiesLPrBody( Computer computer, Intent intent, long matchFlags, List<ResolveInfo> candidates, List<CrossProfileDomainInfo> crossProfileCandidates, int userId, boolean areWebInstantAppsDisabled, boolean debug, boolean resolveForStart, Function<String, PackageStateInternal> pkgSettingFunction)481     private List<ResolveInfo> filterCandidatesWithDomainPreferredActivitiesLPrBody(
482             Computer computer, Intent intent, long matchFlags, List<ResolveInfo> candidates,
483             List<CrossProfileDomainInfo> crossProfileCandidates, int userId,
484             boolean areWebInstantAppsDisabled, boolean debug, boolean resolveForStart,
485             Function<String, PackageStateInternal> pkgSettingFunction) {
486         final ArrayList<ResolveInfo> result = new ArrayList<>();
487         final ArrayList<ResolveInfo> matchAllList = new ArrayList<>();
488         final ArrayList<ResolveInfo> undefinedList = new ArrayList<>();
489 
490         // Blocking instant apps is usually done in applyPostResolutionFilter, but since
491         // domain verification can resolve to a single result, which can be an instant app,
492         // it will then be filtered to an empty list in that method. Instead, do blocking
493         // here so that instant apps can be ignored for approval filtering and a lower
494         // priority result chosen instead.
495         final boolean blockInstant = intent.isWebIntent() && areWebInstantAppsDisabled;
496 
497         final int count = candidates.size();
498         // First, try to use approved apps.
499         for (int n = 0; n < count; n++) {
500             ResolveInfo info = candidates.get(n);
501             if (blockInstant && (info.isInstantAppAvailable
502                     || computer.isInstantAppInternal(info.activityInfo.packageName, userId,
503                     Process.SYSTEM_UID))) {
504                 continue;
505             }
506 
507             // Add to the special match all list (Browser use case)
508             if (info.handleAllWebDataURI) {
509                 matchAllList.add(info);
510             } else {
511                 undefinedList.add(info);
512             }
513         }
514 
515         // We'll want to include browser possibilities in a few cases
516         boolean includeBrowser = false;
517 
518         /**
519          * Grouping CrossProfileDomainInfo based on target user
520          */
521         SparseArray<List<CrossProfileDomainInfo>> categorizeResolveInfoByTargetUser =
522                 new SparseArray<>();
523         if (crossProfileCandidates != null && !crossProfileCandidates.isEmpty()) {
524             for (int index = 0; index < crossProfileCandidates.size(); index++) {
525                 CrossProfileDomainInfo crossProfileDomainInfo = crossProfileCandidates.get(index);
526                 if (!categorizeResolveInfoByTargetUser
527                         .contains(crossProfileDomainInfo.mTargetUserId)) {
528                     categorizeResolveInfoByTargetUser.put(crossProfileDomainInfo.mTargetUserId,
529                             new ArrayList<>());
530                 }
531                 categorizeResolveInfoByTargetUser.get(crossProfileDomainInfo.mTargetUserId)
532                         .add(crossProfileDomainInfo);
533             }
534         }
535 
536         if (!DomainVerificationUtils.isDomainVerificationIntent(intent, matchFlags)) {
537             result.addAll(undefinedList);
538 
539             // calling cross profile strategy to filter corresponding results
540             result.addAll(filterCrossProfileCandidatesWithDomainPreferredActivities(computer,
541                     intent, matchFlags, categorizeResolveInfoByTargetUser, userId,
542                     DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE, resolveForStart));
543             includeBrowser = true;
544         } else {
545             Pair<List<ResolveInfo>, Integer> infosAndLevel = mDomainVerificationManager
546                     .filterToApprovedApp(intent, undefinedList, userId, pkgSettingFunction);
547             List<ResolveInfo> approvedInfos = infosAndLevel.first;
548             Integer highestApproval = infosAndLevel.second;
549 
550             // If no apps are approved for the domain, resolve only to browsers
551             if (approvedInfos.isEmpty()) {
552                 includeBrowser = true;
553                 // calling cross profile strategy to filter corresponding results
554                 result.addAll(filterCrossProfileCandidatesWithDomainPreferredActivities(computer,
555                         intent, matchFlags, categorizeResolveInfoByTargetUser, userId,
556                         DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE, resolveForStart));
557             } else {
558                 result.addAll(approvedInfos);
559 
560                 // If the other profile has an app that's higher approval, add it
561                 // calling cross profile strategy to filter corresponding results
562                 result.addAll(filterCrossProfileCandidatesWithDomainPreferredActivities(computer,
563                         intent, matchFlags, categorizeResolveInfoByTargetUser, userId,
564                         highestApproval, resolveForStart));
565             }
566         }
567 
568         if (includeBrowser) {
569             // Also add browsers (all of them or only the default one)
570             if (DEBUG_DOMAIN_VERIFICATION) {
571                 Slog.v(TAG, " ...including browsers in candidate set");
572             }
573             if ((matchFlags & MATCH_ALL) != 0) {
574                 result.addAll(matchAllList);
575             } else {
576                 // Browser/generic handling case. If there's a default browser, go straight
577                 // to that (but only if there is no other higher-priority match).
578                 final String defaultBrowserPackageName = mDefaultAppProvider.getDefaultBrowser(
579                         userId);
580                 int maxMatchPrio = 0;
581                 ResolveInfo defaultBrowserMatch = null;
582                 final int numCandidates = matchAllList.size();
583                 for (int n = 0; n < numCandidates; n++) {
584                     ResolveInfo info = matchAllList.get(n);
585                     // track the highest overall match priority...
586                     if (info.priority > maxMatchPrio) {
587                         maxMatchPrio = info.priority;
588                     }
589                     // ...and the highest-priority default browser match
590                     if (info.activityInfo.packageName.equals(defaultBrowserPackageName)) {
591                         if (defaultBrowserMatch == null
592                                 || (defaultBrowserMatch.priority < info.priority)) {
593                             if (debug) {
594                                 Slog.v(TAG, "Considering default browser match " + info);
595                             }
596                             defaultBrowserMatch = info;
597                         }
598                     }
599                 }
600                 if (defaultBrowserMatch != null
601                         && defaultBrowserMatch.priority >= maxMatchPrio
602                         && !TextUtils.isEmpty(defaultBrowserPackageName)) {
603                     if (debug) {
604                         Slog.v(TAG, "Default browser match " + defaultBrowserMatch);
605                     }
606                     result.add(defaultBrowserMatch);
607                 } else {
608                     result.addAll(matchAllList);
609                 }
610             }
611 
612             // If there is nothing selected, add all candidates
613             if (result.size() == 0) {
614                 result.addAll(candidates);
615             }
616         }
617         return result;
618     }
619 
620     /**
621      * Filter cross profile results by calling their respective strategy
622      * @param computer {@link Computer} instance
623      * @param intent request
624      * @param flags for intent request
625      * @param categorizeResolveInfoByTargetUser group of targetuser and its corresponding
626      *                                          CrossProfileDomainInfos
627      * @param sourceUserId user id for intent
628      * @param highestApprovalLevel domain approval level
629      * @param resolveForStart true if intent is for resolution
630      * @return list of ResolveInfos
631      */
filterCrossProfileCandidatesWithDomainPreferredActivities( Computer computer, Intent intent, long flags, SparseArray<List<CrossProfileDomainInfo>> categorizeResolveInfoByTargetUser, int sourceUserId, int highestApprovalLevel, boolean resolveForStart)632     private List<ResolveInfo> filterCrossProfileCandidatesWithDomainPreferredActivities(
633             Computer computer, Intent intent, long flags, SparseArray<List<CrossProfileDomainInfo>>
634             categorizeResolveInfoByTargetUser, int sourceUserId, int highestApprovalLevel,
635             boolean resolveForStart) {
636 
637         List<CrossProfileDomainInfo> crossProfileDomainInfos = new ArrayList<>();
638 
639         for (int index = 0; index < categorizeResolveInfoByTargetUser.size(); index++) {
640 
641             // if resolve info does not target user or has default value, add results as they are.
642             if (categorizeResolveInfoByTargetUser.keyAt(index) == -2) {
643                 crossProfileDomainInfos.addAll(categorizeResolveInfoByTargetUser.valueAt(index));
644             } else {
645                 // finding cross profile strategy based on source and target user
646                 CrossProfileResolver crossProfileIntentResolver =
647                         chooseCrossProfileResolver(computer, sourceUserId,
648                                 categorizeResolveInfoByTargetUser.keyAt(index), resolveForStart,
649                                 flags);
650                 // if strategy is available call it and add its filtered results
651                 if (crossProfileIntentResolver != null) {
652                     crossProfileDomainInfos.addAll(crossProfileIntentResolver
653                             .filterResolveInfoWithDomainPreferredActivity(intent,
654                                     categorizeResolveInfoByTargetUser.valueAt(index),
655                                     flags, sourceUserId, categorizeResolveInfoByTargetUser
656                                             .keyAt(index), highestApprovalLevel));
657                 } else {
658                     // if strategy is not available call it, add the results
659                     crossProfileDomainInfos.addAll(categorizeResolveInfoByTargetUser
660                             .valueAt(index));
661                 }
662             }
663         }
664         return resolveInfoFromCrossProfileDomainInfo(crossProfileDomainInfos);
665     }
666 
667     /**
668      * Extract ResolveInfo from CrossProfileDomainInfo
669      * @param crossProfileDomainInfos cross profile results
670      * @return list of ResolveInfo
671      */
resolveInfoFromCrossProfileDomainInfo(List<CrossProfileDomainInfo> crossProfileDomainInfos)672     private List<ResolveInfo> resolveInfoFromCrossProfileDomainInfo(List<CrossProfileDomainInfo>
673             crossProfileDomainInfos) {
674         List<ResolveInfo> resolveInfoList = new ArrayList<>();
675 
676         for (int infoIndex = 0; infoIndex < crossProfileDomainInfos.size(); infoIndex++) {
677             resolveInfoList.add(crossProfileDomainInfos.get(infoIndex).mResolveInfo);
678         }
679 
680         return resolveInfoList;
681     }
682 
683     /**
684      * @param crossProfileDomainInfos list of cross profile domain info in descending priority order
685      * @return if the list contains a resolve info with non-negative priority
686      */
hasNonNegativePriority(List<CrossProfileDomainInfo> crossProfileDomainInfos)687     private boolean hasNonNegativePriority(List<CrossProfileDomainInfo> crossProfileDomainInfos) {
688         return crossProfileDomainInfos.size() > 0
689                 && crossProfileDomainInfos.get(0).mResolveInfo != null
690                 && crossProfileDomainInfos.get(0).mResolveInfo.priority >= 0;
691     }
692 
693     /**
694      * Deciding if we need to user {@link NoFilteringResolver} based on source and target user
695      * @param sourceUserId id of initiating user
696      * @param targetUserId id of cross profile linked user
697      * @return true if {@link NoFilteringResolver} is applicable in this case.
698      */
shouldUseNoFilteringResolver(@serIdInt int sourceUserId, @UserIdInt int targetUserId)699     private boolean shouldUseNoFilteringResolver(@UserIdInt int sourceUserId,
700             @UserIdInt int targetUserId) {
701         return isNoFilteringPropertyConfiguredForUser(sourceUserId)
702                 || isNoFilteringPropertyConfiguredForUser(targetUserId);
703     }
704 
705     /**
706      * Check if configure property for cross profile intent resolution strategy for user is
707      * {@link UserProperties#CROSS_PROFILE_INTENT_RESOLUTION_STRATEGY_NO_FILTERING}
708      * @param userId id of user to check for property
709      * @return true if user have property set to
710      * {@link UserProperties#CROSS_PROFILE_INTENT_RESOLUTION_STRATEGY_NO_FILTERING}
711      */
isNoFilteringPropertyConfiguredForUser(@serIdInt int userId)712     private boolean isNoFilteringPropertyConfiguredForUser(@UserIdInt int userId) {
713         if (!mUserManager.isProfile(userId)) return false;
714         UserProperties userProperties = mUserManagerInternal.getUserProperties(userId);
715         if (userProperties == null) return false;
716 
717         return userProperties.getCrossProfileIntentResolutionStrategy()
718                 == UserProperties.CROSS_PROFILE_INTENT_RESOLUTION_STRATEGY_NO_FILTERING;
719     }
720 }
721