diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java index 15cd639276481..423c276091f38 100644 --- a/services/core/java/com/android/server/pm/Computer.java +++ b/services/core/java/com/android/server/pm/Computer.java @@ -91,7 +91,7 @@ import java.util.Set; * other hand, not overriding in {@link ComputerLocked} may leave a function walking * unstable data. */ -@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public interface Computer extends PackageDataSnapshot { int getVersion(); diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index ed846dbbfc1cd..bef87c4e55212 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -30,7 +30,6 @@ import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED; import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER; -import static android.content.pm.PackageManager.MATCH_ALL; import static android.content.pm.PackageManager.MATCH_ANY_USER; import static android.content.pm.PackageManager.MATCH_APEX; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; @@ -49,7 +48,6 @@ import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER; import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE; import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_PARENT; -import static com.android.server.pm.PackageManagerService.DEBUG_DOMAIN_VERIFICATION; import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL; import static com.android.server.pm.PackageManagerService.DEBUG_INSTANT; import static com.android.server.pm.PackageManagerService.DEBUG_PACKAGE_INFO; @@ -116,7 +114,6 @@ import android.util.MathUtils; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; -import android.util.SparseBooleanArray; import android.util.TypedXmlSerializer; import android.util.Xml; import android.util.proto.ProtoOutputStream; @@ -145,7 +142,6 @@ import com.android.server.pm.pkg.component.ParsedProvider; import com.android.server.pm.pkg.component.ParsedService; import com.android.server.pm.resolution.ComponentResolverApi; import com.android.server.pm.verify.domain.DomainVerificationManagerInternal; -import com.android.server.pm.verify.domain.DomainVerificationUtils; import com.android.server.uri.UriGrantsManagerInternal; import com.android.server.utils.WatchedArrayMap; import com.android.server.utils.WatchedLongSparseArray; @@ -411,6 +407,7 @@ public class ComputerEngine implements Computer { private final CompilerStats mCompilerStats; private final BackgroundDexOptService mBackgroundDexOptService; private final PackageManagerInternal.ExternalSourcesPolicy mExternalSourcesPolicy; + private final CrossProfileIntentResolverEngine mCrossProfileIntentResolverEngine; // PackageManagerService attributes that are primitives are referenced through the // pms object directly. Primitives are the only attributes so referenced. @@ -464,6 +461,8 @@ public class ComputerEngine implements Computer { mCompilerStats = args.service.mCompilerStats; mBackgroundDexOptService = args.service.mBackgroundDexOptService; mExternalSourcesPolicy = args.service.mExternalSourcesPolicy; + mCrossProfileIntentResolverEngine = new CrossProfileIntentResolverEngine( + mUserManager, mDomainVerificationManager, mDefaultAppProvider); // Used to reference PMS attributes that are primitives and which are not // updated under control of the PMS lock. @@ -735,101 +734,72 @@ public class ComputerEngine implements Computer { // reader boolean sortResult = false; boolean addInstant = false; - List result = null; + List result = new ArrayList<>(); + // crossProfileResults will hold resolve infos from resolution across profiles. + List crossProfileResults = new ArrayList<>(); if (pkgName == null) { - List matchingFilters = - getMatchingCrossProfileIntentFilters(intent, resolvedType, userId); - // Check for results that need to skip the current profile. - ResolveInfo skipProfileInfo = querySkipCurrentProfileIntents(matchingFilters, - intent, resolvedType, flags, userId); - if (skipProfileInfo != null) { - List xpResult = new ArrayList<>(1); - xpResult.add(skipProfileInfo); - return new QueryIntentActivitiesResult( - applyPostResolutionFilter( - filterIfNotSystemUser(xpResult, userId), instantAppPkgName, - allowDynamicSplits, filterCallingUid, resolveForStart, userId, - intent)); + if (!mCrossProfileIntentResolverEngine.shouldSkipCurrentProfile(this, intent, + resolvedType, userId)) { + /* + Check for results in the current profile only if there is no + {@link CrossProfileIntentFilter} for user with flag + {@link PackageManager.SKIP_CURRENT_PROFILE} set. + */ + result.addAll(filterIfNotSystemUser(mComponentResolver.queryActivities(this, + intent, resolvedType, flags, userId), userId)); } - - // Check for results in the current profile. - result = filterIfNotSystemUser(mComponentResolver.queryActivities(this, - intent, resolvedType, flags, userId), userId); addInstant = isInstantAppResolutionAllowed(intent, result, userId, false /*skipPackageCheck*/, flags); - // Check for cross profile results. + boolean hasNonNegativePriorityResult = hasNonNegativePriority(result); - CrossProfileDomainInfo specificXpInfo = queryCrossProfileIntents( - matchingFilters, intent, resolvedType, flags, userId, - hasNonNegativePriorityResult); - if (intent.hasWebURI()) { - CrossProfileDomainInfo generalXpInfo = null; - final UserInfo parent = getProfileParent(userId); - if (parent != null) { - generalXpInfo = getCrossProfileDomainPreferredLpr(intent, resolvedType, - flags, userId, parent.id); - } - // Generalized cross profile intents take precedence over specific. - // Note that this is the opposite of the intuitive order. - CrossProfileDomainInfo prioritizedXpInfo = - generalXpInfo != null ? generalXpInfo : specificXpInfo; - - if (!addInstant) { - if (result.isEmpty() && prioritizedXpInfo != null) { - // No result in current profile, but found candidate in parent user. - // And we are not going to add ephemeral app, so we can return the - // result straight away. - result.add(prioritizedXpInfo.mResolveInfo); - return new QueryIntentActivitiesResult( - applyPostResolutionFilter(result, instantAppPkgName, - allowDynamicSplits, filterCallingUid, resolveForStart, - userId, intent)); - } else if (result.size() <= 1 && prioritizedXpInfo == null) { - // No result in parent user and <= 1 result in current profile, and we - // are not going to add ephemeral app, so we can return the result - // without further processing. - return new QueryIntentActivitiesResult( - applyPostResolutionFilter(result, instantAppPkgName, - allowDynamicSplits, filterCallingUid, resolveForStart, - userId, intent)); - } - } - - // We have more than one candidate (combining results from current and parent - // profile), so we need filtering and sorting. - result = filterCandidatesWithDomainPreferredActivitiesLPr( - intent, flags, result, prioritizedXpInfo, userId); - sortResult = true; - } else { - // If not web Intent, just add result to candidate set and let ResolverActivity - // figure it out. - if (specificXpInfo != null) { - result.add(specificXpInfo.mResolveInfo); - sortResult = true; - } - } + /* + Calling {@link com.android.server.pm.CrossProfileIntentResolverEngine#resolveIntent} to + get list of {@link CrossProfileDomainInfo} which have {@link ResolveInfo}s from linked + profiles. + */ + crossProfileResults = mCrossProfileIntentResolverEngine.resolveIntent(this, intent, + resolvedType, userId, flags, pkgName, hasNonNegativePriorityResult, + mSettings::getPackage); + if (intent.hasWebURI() || !crossProfileResults.isEmpty()) sortResult = true; } else { final PackageStateInternal setting = getPackageStateInternal(pkgName, Process.SYSTEM_UID); - result = null; + if (setting != null && setting.getAndroidPackage() != null && (resolveForStart || !shouldFilterApplication(setting, filterCallingUid, userId))) { - result = filterIfNotSystemUser(mComponentResolver.queryActivities(this, + result.addAll(filterIfNotSystemUser(mComponentResolver.queryActivities(this, intent, resolvedType, flags, setting.getAndroidPackage().getActivities(), - userId), userId); + userId), userId)); } if (result == null || result.size() == 0) { // the caller wants to resolve for a particular package; however, there // were no installed results, so, try to find an ephemeral result addInstant = isInstantAppResolutionAllowed(intent, null /*result*/, userId, true /*skipPackageCheck*/, flags); - if (result == null) { - result = new ArrayList<>(); - } } + /* + Calling {@link com.android.server.pm.CrossProfileIntentResolverEngine#resolveIntent} to + get list of {@link CrossProfileDomainInfo} which have {@link ResolveInfo}s from linked + profiles. + */ + crossProfileResults = mCrossProfileIntentResolverEngine.resolveIntent(this, intent, + resolvedType, userId, flags, pkgName, false, + mSettings::getPackage); } - return new QueryIntentActivitiesResult(sortResult, addInstant, result); + + /* + Calling {@link com.android.server.pm. + CrossProfileIntentResolverEngine#combineFilterAndCreateQueryAcitivitesResponse} to + combine results from current and cross profiles. This also filters any resolve info + based on domain preference(if required). + */ + return mCrossProfileIntentResolverEngine + .combineFilterAndCreateQueryActivitiesResponse(this, intent, resolvedType, + instantAppPkgName, pkgName, allowDynamicSplits, flags, userId, + filterCallingUid, resolveForStart, result, crossProfileResults, + areWebInstantAppsDisabled(userId), addInstant, sortResult, + mSettings::getPackage); } /** @@ -1046,126 +1016,6 @@ public class ComputerEngine implements Computer { return null; } - protected ArrayList filterCandidatesWithDomainPreferredActivitiesLPrBody( - Intent intent, long matchFlags, List candidates, - CrossProfileDomainInfo xpDomainInfo, int userId, boolean debug) { - final ArrayList result = new ArrayList<>(); - final ArrayList matchAllList = new ArrayList<>(); - final ArrayList undefinedList = new ArrayList<>(); - - // Blocking instant apps is usually done in applyPostResolutionFilter, but since - // domain verification can resolve to a single result, which can be an instant app, - // it will then be filtered to an empty list in that method. Instead, do blocking - // here so that instant apps can be ignored for approval filtering and a lower - // priority result chosen instead. - final boolean blockInstant = intent.isWebIntent() && areWebInstantAppsDisabled(userId); - - final int count = candidates.size(); - // First, try to use approved apps. - for (int n = 0; n < count; n++) { - ResolveInfo info = candidates.get(n); - if (blockInstant && (info.isInstantAppAvailable - || isInstantAppInternal(info.activityInfo.packageName, userId, - Process.SYSTEM_UID))) { - continue; - } - - // Add to the special match all list (Browser use case) - if (info.handleAllWebDataURI) { - matchAllList.add(info); - } else { - undefinedList.add(info); - } - } - - // We'll want to include browser possibilities in a few cases - boolean includeBrowser = false; - - if (!DomainVerificationUtils.isDomainVerificationIntent(intent, matchFlags)) { - result.addAll(undefinedList); - // Maybe add one for the other profile. - if (xpDomainInfo != null && xpDomainInfo.mHighestApprovalLevel - > DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE) { - result.add(xpDomainInfo.mResolveInfo); - } - includeBrowser = true; - } else { - Pair, Integer> infosAndLevel = mDomainVerificationManager - .filterToApprovedApp(intent, undefinedList, userId, - mSettings::getPackage); - List approvedInfos = infosAndLevel.first; - Integer highestApproval = infosAndLevel.second; - - // If no apps are approved for the domain, resolve only to browsers - if (approvedInfos.isEmpty()) { - includeBrowser = true; - if (xpDomainInfo != null && xpDomainInfo.mHighestApprovalLevel - > DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE) { - result.add(xpDomainInfo.mResolveInfo); - } - } else { - result.addAll(approvedInfos); - - // If the other profile has an app that's higher approval, add it - if (xpDomainInfo != null - && xpDomainInfo.mHighestApprovalLevel > highestApproval) { - result.add(xpDomainInfo.mResolveInfo); - } - } - } - - if (includeBrowser) { - // Also add browsers (all of them or only the default one) - if (DEBUG_DOMAIN_VERIFICATION) { - Slog.v(TAG, " ...including browsers in candidate set"); - } - if ((matchFlags & MATCH_ALL) != 0) { - result.addAll(matchAllList); - } else { - // Browser/generic handling case. If there's a default browser, go straight - // to that (but only if there is no other higher-priority match). - final String defaultBrowserPackageName = mDefaultAppProvider.getDefaultBrowser( - userId); - int maxMatchPrio = 0; - ResolveInfo defaultBrowserMatch = null; - final int numCandidates = matchAllList.size(); - for (int n = 0; n < numCandidates; n++) { - ResolveInfo info = matchAllList.get(n); - // track the highest overall match priority... - if (info.priority > maxMatchPrio) { - maxMatchPrio = info.priority; - } - // ...and the highest-priority default browser match - if (info.activityInfo.packageName.equals(defaultBrowserPackageName)) { - if (defaultBrowserMatch == null - || (defaultBrowserMatch.priority < info.priority)) { - if (debug) { - Slog.v(TAG, "Considering default browser match " + info); - } - defaultBrowserMatch = info; - } - } - } - if (defaultBrowserMatch != null - && defaultBrowserMatch.priority >= maxMatchPrio - && !TextUtils.isEmpty(defaultBrowserPackageName)) { - if (debug) { - Slog.v(TAG, "Default browser match " + defaultBrowserMatch); - } - result.add(defaultBrowserMatch); - } else { - result.addAll(matchAllList); - } - } - - // If there is nothing selected, add all candidates - if (result.size() == 0) { - result.addAll(candidates); - } - } - return result; - } - /** * Report the 'Home' activity which is currently set as "always use this one". If non is set * then reports the most likely home activity or null if there are more than one. @@ -1279,7 +1129,8 @@ public class ComputerEngine implements Computer { if (result == null) { result = new CrossProfileDomainInfo(createForwardingResolveInfoUnchecked( - new WatchedIntentFilter(), sourceUserId, parentUserId), approvalLevel); + new WatchedIntentFilter(), sourceUserId, parentUserId), approvalLevel, + parentUserId); } else { result.mHighestApprovalLevel = Math.max(approvalLevel, result.mHighestApprovalLevel); @@ -1303,7 +1154,8 @@ public class ComputerEngine implements Computer { Intent intent, String resolvedType, int userId) { CrossProfileIntentResolver resolver = mSettings.getCrossProfileIntentResolver(userId); if (resolver != null) { - return resolver.queryIntent(this, intent, resolvedType, false /*defaultOnly*/, userId); + return resolver.queryIntent(this, intent, resolvedType, false /*defaultOnly*/, + userId); } return null; } @@ -1467,30 +1319,6 @@ public class ComputerEngine implements Computer { return resolveInfos; } - private List filterCandidatesWithDomainPreferredActivitiesLPr(Intent intent, - long matchFlags, List candidates, CrossProfileDomainInfo xpDomainInfo, - int userId) { - final boolean debug = (intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0; - - if (DEBUG_PREFERRED || DEBUG_DOMAIN_VERIFICATION) { - Slog.v(TAG, "Filtering results with preferred activities. Candidates count: " - + candidates.size()); - } - - final ArrayList result = - filterCandidatesWithDomainPreferredActivitiesLPrBody( - intent, matchFlags, candidates, xpDomainInfo, userId, debug); - - if (DEBUG_PREFERRED || DEBUG_DOMAIN_VERIFICATION) { - Slog.v(TAG, "Filtered results with preferred activities. New candidates count: " - + result.size()); - for (ResolveInfo info : result) { - Slog.v(TAG, " + " + info.activityInfo); - } - } - return result; - } - /** * Filter out activities with systemUserOnly flag set, when current user is not System. * @@ -1930,63 +1758,6 @@ public class ComputerEngine implements Computer { return new ParceledListSlice<>(list); } - /** - * If the filter's target user can handle the intent and is enabled: a [ResolveInfo] that - * will forward the intent to the filter's target user, along with the highest approval of - * any handler in the target user. Otherwise, returns null. - */ - @Nullable - private CrossProfileDomainInfo createForwardingResolveInfo( - @NonNull CrossProfileIntentFilter filter, @NonNull Intent intent, - @Nullable String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags, - int sourceUserId) { - int targetUserId = filter.getTargetUserId(); - if (!isUserEnabled(targetUserId)) { - return null; - } - - List resultTargetUser = mComponentResolver.queryActivities(this, intent, - resolvedType, flags, targetUserId); - if (CollectionUtils.isEmpty(resultTargetUser)) { - return null; - } - - ResolveInfo forwardingInfo = null; - for (int i = resultTargetUser.size() - 1; i >= 0; i--) { - ResolveInfo targetUserResolveInfo = resultTargetUser.get(i); - if ((targetUserResolveInfo.activityInfo.applicationInfo.flags - & ApplicationInfo.FLAG_SUSPENDED) == 0) { - forwardingInfo = createForwardingResolveInfoUnchecked(filter, sourceUserId, - targetUserId); - break; - } - } - - if (forwardingInfo == null) { - // If all the matches in the target profile are suspended, return null. - return null; - } - - int highestApprovalLevel = DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE; - - int size = resultTargetUser.size(); - for (int i = 0; i < size; i++) { - ResolveInfo riTargetUser = resultTargetUser.get(i); - if (riTargetUser.handleAllWebDataURI) { - continue; - } - String packageName = riTargetUser.activityInfo.packageName; - PackageStateInternal ps = mSettings.getPackage(packageName); - if (ps == null) { - continue; - } - highestApprovalLevel = Math.max(highestApprovalLevel, mDomainVerificationManager - .approvalLevelForDomain(ps, intent, flags, targetUserId)); - } - - return new CrossProfileDomainInfo(forwardingInfo, highestApprovalLevel); - } - public final ResolveInfo createForwardingResolveInfoUnchecked(WatchedIntentFilter filter, int sourceUserId, int targetUserId) { ResolveInfo forwardingResolveInfo = new ResolveInfo(); @@ -2022,83 +1793,6 @@ public class ComputerEngine implements Computer { return forwardingResolveInfo; } - // Return matching ResolveInfo in target user if any. - @Nullable - private CrossProfileDomainInfo queryCrossProfileIntents( - List matchingFilters, Intent intent, String resolvedType, - long flags, int sourceUserId, boolean matchInCurrentProfile) { - if (matchingFilters == null) { - return null; - } - // Two {@link CrossProfileIntentFilter}s can have the same targetUserId and - // match the same intent. For performance reasons, it is better not to - // run queryIntent twice for the same userId - SparseBooleanArray alreadyTriedUserIds = new SparseBooleanArray(); - - CrossProfileDomainInfo resultInfo = null; - - int size = matchingFilters.size(); - for (int i = 0; i < size; i++) { - CrossProfileIntentFilter filter = matchingFilters.get(i); - int targetUserId = filter.getTargetUserId(); - boolean skipCurrentProfile = - (filter.getFlags() & PackageManager.SKIP_CURRENT_PROFILE) != 0; - boolean skipCurrentProfileIfNoMatchFound = - (filter.getFlags() & PackageManager.ONLY_IF_NO_MATCH_FOUND) != 0; - if (!skipCurrentProfile && !alreadyTriedUserIds.get(targetUserId) - && (!skipCurrentProfileIfNoMatchFound || !matchInCurrentProfile)) { - // Checking if there are activities in the target user that can handle the - // intent. - CrossProfileDomainInfo info = createForwardingResolveInfo(filter, intent, - resolvedType, flags, sourceUserId); - if (info != null) { - resultInfo = info; - break; - } - alreadyTriedUserIds.put(targetUserId, true); - } - } - - if (resultInfo == null) { - return null; - } - - ResolveInfo forwardingResolveInfo = resultInfo.mResolveInfo; - if (!isUserEnabled(forwardingResolveInfo.targetUserId)) { - return null; - } - - List filteredResult = - filterIfNotSystemUser(Collections.singletonList(forwardingResolveInfo), - sourceUserId); - if (filteredResult.isEmpty()) { - return null; - } - - return resultInfo; - } - - private ResolveInfo querySkipCurrentProfileIntents( - List matchingFilters, Intent intent, String resolvedType, - long flags, int sourceUserId) { - if (matchingFilters != null) { - int size = matchingFilters.size(); - for (int i = 0; i < size; i++) { - CrossProfileIntentFilter filter = matchingFilters.get(i); - if ((filter.getFlags() & PackageManager.SKIP_CURRENT_PROFILE) != 0) { - // Checking if there are activities in the target user that can handle the - // intent. - CrossProfileDomainInfo info = createForwardingResolveInfo(filter, intent, - resolvedType, flags, sourceUserId); - if (info != null) { - return info.mResolveInfo; - } - } - } - } - return null; - } - public final ServiceInfo getServiceInfo(ComponentName component, @PackageManager.ResolveInfoFlagsBits long flags, int userId) { if (!mUserManager.exists(userId)) return null; @@ -2740,16 +2434,6 @@ public class ComputerEngine implements Computer { } } - private boolean isUserEnabled(int userId) { - final long callingId = Binder.clearCallingIdentity(); - try { - UserInfo userInfo = mUserManager.getUserInfo(userId); - return userInfo != null && userInfo.isEnabled(); - } finally { - Binder.restoreCallingIdentity(callingId); - } - } - /** * Returns whether or not access to the application should be filtered. *

@@ -5691,13 +5375,9 @@ public class ComputerEngine implements Computer { @UserIdInt int sourceUserId, @UserIdInt int targetUserId) { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); - List matches = - getMatchingCrossProfileIntentFilters(intent, resolvedType, sourceUserId); - if (matches != null) { - int size = matches.size(); - for (int i = 0; i < size; i++) { - if (matches.get(i).getTargetUserId() == targetUserId) return true; - } + if (mCrossProfileIntentResolverEngine.canReachTo(this, intent, resolvedType, + sourceUserId, targetUserId)) { + return true; } if (intent.hasWebURI()) { // cross-profile app linking works only towards the parent. diff --git a/services/core/java/com/android/server/pm/CrossProfileDomainInfo.java b/services/core/java/com/android/server/pm/CrossProfileDomainInfo.java index 31f4fa3baa58b..72f3afc35e995 100644 --- a/services/core/java/com/android/server/pm/CrossProfileDomainInfo.java +++ b/services/core/java/com/android/server/pm/CrossProfileDomainInfo.java @@ -16,12 +16,23 @@ package com.android.server.pm; +import android.annotation.UserIdInt; import android.content.pm.ResolveInfo; +import android.os.UserHandle; public final class CrossProfileDomainInfo { /* ResolveInfo for IntentForwarderActivity to send the intent to the other profile */ ResolveInfo mResolveInfo; int mHighestApprovalLevel; + @UserIdInt + int mTargetUserId = UserHandle.USER_CURRENT; // default as current user + + CrossProfileDomainInfo(ResolveInfo resolveInfo, int highestApprovalLevel, @UserIdInt + int targetUserId) { + this.mResolveInfo = resolveInfo; + this.mHighestApprovalLevel = highestApprovalLevel; + this.mTargetUserId = targetUserId; + } CrossProfileDomainInfo(ResolveInfo resolveInfo, int highestApprovalLevel) { this.mResolveInfo = resolveInfo; @@ -33,6 +44,7 @@ public final class CrossProfileDomainInfo { return "CrossProfileDomainInfo{" + "resolveInfo=" + mResolveInfo + ", highestApprovalLevel=" + mHighestApprovalLevel + + ", targetUserId= " + mTargetUserId + '}'; } } diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java new file mode 100644 index 0000000000000..3f82923763e21 --- /dev/null +++ b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java @@ -0,0 +1,575 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import static android.content.pm.PackageManager.MATCH_ALL; + +import static com.android.server.pm.PackageManagerService.DEBUG_DOMAIN_VERIFICATION; +import static com.android.server.pm.PackageManagerService.DEBUG_PREFERRED; +import static com.android.server.pm.PackageManagerService.TAG; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.UserInfo; +import android.os.Process; +import android.text.TextUtils; +import android.util.Pair; +import android.util.Slog; +import android.util.SparseArray; + +import com.android.server.LocalServices; +import com.android.server.pm.pkg.PackageStateInternal; +import com.android.server.pm.verify.domain.DomainVerificationManagerInternal; +import com.android.server.pm.verify.domain.DomainVerificationUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * Rule based engine which decides strategy to be used for source,target pair and does cross profile + * intent resolution. Currently, we have only default and clone strategy. The major known use-case + * for default is work profile. + */ +public class CrossProfileIntentResolverEngine { + + private final UserManagerService mUserManager; + private final DomainVerificationManagerInternal mDomainVerificationManager; + private final DefaultAppProvider mDefaultAppProvider; + + public CrossProfileIntentResolverEngine(UserManagerService userManager, + DomainVerificationManagerInternal domainVerificationManager, + DefaultAppProvider defaultAppProvider) { + mUserManager = userManager; + mDomainVerificationManager = domainVerificationManager; + mDefaultAppProvider = defaultAppProvider; + } + + /** + * Returns the list of {@link CrossProfileDomainInfo} which contains {@link ResolveInfo} from + * profiles linked directly/indirectly to user. Work-Owner as well as Clone-Owner + * are directly related as they are child of owner. Work-Clone are indirectly linked through + * owner profile. + * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi} + * @param intent request + * @param resolvedType the MIME data type of intent request + * @param userId source user for which intent request is called + * @param flags used for intent resolution + * @param pkgName the application package name this Intent is limited to. + * @param hasNonNegativePriorityResult signifies if current profile have any non-negative(active + * and valid) ResolveInfo in current profile. + * @param pkgSettingFunction function to find PackageStateInternal for given package + * @return list of {@link CrossProfileDomainInfo} from linked profiles. + */ + public List resolveIntent(@NonNull Computer computer, Intent intent, + String resolvedType, int userId, long flags, String pkgName, + boolean hasNonNegativePriorityResult, + Function pkgSettingFunction) { + return resolveIntentInternal(computer, intent, resolvedType, userId, flags, pkgName, + hasNonNegativePriorityResult, pkgSettingFunction); + } + + /** + * Resolves intent in directly linked profiles and return list of {@link CrossProfileDomainInfo} + * which contains {@link ResolveInfo}. This would also recursively call profiles not directly + * linked. + * + * It first finds {@link CrossProfileIntentFilter} configured in current profile to find list of + * target user profiles that can serve current intent request. It uses corresponding strategy + * for each pair (source,target) user to resolve intent from target profile and returns combined + * results. + * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi} + * @param intent request + * @param resolvedType the MIME data type of intent request + * @param userId source user for which intent request is called + * @param flags used for intent resolution + * @param pkgName the application package name this Intent is limited to. + * @param hasNonNegativePriorityResult signifies if current profile have any non-negative(active + * and valid) ResolveInfo in current profile. + * @param pkgSettingFunction function to find PackageStateInternal for given package + * @return list of {@link CrossProfileDomainInfo} from linked profiles. + */ + private List resolveIntentInternal(@NonNull Computer computer, + Intent intent, String resolvedType, int userId, long flags, String pkgName, + boolean hasNonNegativePriorityResult, + Function pkgSettingFunction) { + + List crossProfileDomainInfos = new ArrayList<>(); + + List matchingFilters = + computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, userId); + + if (matchingFilters == null || matchingFilters.isEmpty()) return crossProfileDomainInfos; + + UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class); + UserInfo sourceUserInfo = umInternal.getUserInfo(userId); + + // Grouping the CrossProfileIntentFilters based on targerId + SparseArray> crossProfileIntentFiltersByUser = + new SparseArray<>(); + + for (int index = 0; index < matchingFilters.size(); index++) { + CrossProfileIntentFilter crossProfileIntentFilter = matchingFilters.get(index); + + if (!crossProfileIntentFiltersByUser + .contains(crossProfileIntentFilter.mTargetUserId)) { + crossProfileIntentFiltersByUser.put(crossProfileIntentFilter.mTargetUserId, + new ArrayList<>()); + } + crossProfileIntentFiltersByUser.get(crossProfileIntentFilter.mTargetUserId) + .add(crossProfileIntentFilter); + } + + /* + For each target user, we would call their corresponding strategy + {@link CrossProfileResolver} to resolve intent in corresponding user + */ + for (int index = 0; index < crossProfileIntentFiltersByUser.size(); index++) { + + UserInfo targetUserInfo = umInternal.getUserInfo(crossProfileIntentFiltersByUser + .keyAt(index)); + + // Choosing strategy based on source and target user + CrossProfileResolver crossProfileResolver = + chooseCrossProfileResolver(computer, sourceUserInfo, targetUserInfo); + + /* + If {@link CrossProfileResolver} is available for source,target pair we will call it to + get {@link CrossProfileDomainInfo}s from that user. + */ + if (crossProfileResolver != null) { + List crossProfileInfos = crossProfileResolver + .resolveIntent(computer, intent, resolvedType, userId, + crossProfileIntentFiltersByUser.keyAt(index), flags, pkgName, + crossProfileIntentFiltersByUser.valueAt(index), + hasNonNegativePriorityResult, pkgSettingFunction); + crossProfileDomainInfos.addAll(crossProfileInfos); + } + } + + return crossProfileDomainInfos; + } + + + /** + * Returns {@link CrossProfileResolver} strategy based on source and target user + * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi} + * @param sourceUserInfo source user + * @param targetUserInfo target user + * @return {@code CrossProfileResolver} which has value if source and target have + * strategy configured otherwise null. + */ + @SuppressWarnings("unused") + private CrossProfileResolver chooseCrossProfileResolver(@NonNull Computer computer, + UserInfo sourceUserInfo, UserInfo targetUserInfo) { + return new DefaultCrossProfileResolver(computer.getComponentResolver(), + mUserManager, mDomainVerificationManager); + } + + /** + * Returns true if we source user can reach target user for given intent. The source can + * directly or indirectly reach to target. + * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi} + * @param intent request + * @param resolvedType the MIME data type of intent request + * @param sourceUserId source user + * @param targetUserId target user + * @return true if we source user can reach target user for given intent + */ + public boolean canReachTo(@NonNull Computer computer, @NonNull Intent intent, + @Nullable String resolvedType, @UserIdInt int sourceUserId, + @UserIdInt int targetUserId) { + return canReachToInternal(computer, intent, resolvedType, sourceUserId, targetUserId); + } + + /** + * Returns true if we source user can reach target user for given intent. The source can + * directly or indirectly reach to target. This will perform depth first search to check if + * source can reach target. + * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi} + * @param intent request + * @param resolvedType the MIME data type of intent request + * @param sourceUserId source user + * @param targetUserId target user + * @return true if we source user can reach target user for given intent + */ + private boolean canReachToInternal(@NonNull Computer computer, @NonNull Intent intent, + @Nullable String resolvedType, @UserIdInt int sourceUserId, + @UserIdInt int targetUserId) { + if (sourceUserId == targetUserId) return true; + + List matches = + computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, sourceUserId); + if (matches != null) { + for (int index = 0; index < matches.size(); index++) { + CrossProfileIntentFilter crossProfileIntentFilter = matches.get(index); + if (crossProfileIntentFilter.mTargetUserId == targetUserId) { + return true; + } + } + } + return false; + } + + /** + * Checks if any of the matching {@link CrossProfileIntentFilter} suggest we should skip the + * current profile based on flag {@link PackageManager#SKIP_CURRENT_PROFILE}. + * @param computer {@link Computer} instance used to find {@link CrossProfileIntentFilter} + * for user + * @param intent request + * @param resolvedType the MIME data type of intent request + * @param sourceUserId id of initiating user space + * @return boolean if we should skip resolution in current/source profile. + */ + public boolean shouldSkipCurrentProfile(Computer computer, Intent intent, String resolvedType, + int sourceUserId) { + List matches = + computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, sourceUserId); + if (matches != null) { + for (int matchIndex = 0; matchIndex < matches.size(); matchIndex++) { + CrossProfileIntentFilter crossProfileIntentFilter = matches.get(matchIndex); + if ((crossProfileIntentFilter.getFlags() + & PackageManager.SKIP_CURRENT_PROFILE) != 0) { + return true; + } + } + } + return false; + } + + /** + * Combines result from current and cross profile. This also does filtering based on domain(if + * required). + * @param computer {@link Computer} instance + * @param intent request + * @param resolvedType the MIME data type of intent request + * @param instantAppPkgName package name if instant app is allowed + * @param pkgName the application package name this Intent is limited to. + * @param allowDynamicSplits true if dynamic splits is allowed + * @param matchFlags flags for intent request + * @param userId user id of source user + * @param filterCallingUid uid of calling process + * @param resolveForStart true if resolution occurs because an application is starting + * @param candidates resolveInfos from current profile + * @param crossProfileCandidates crossProfileDomainInfos from cross profile, it has ResolveInfo + * @param areWebInstantAppsDisabled true if web instant apps are disabled + * @param addInstant true if instant apps are allowed + * @param sortResult true if caller would need to sort the results + * @param pkgSettingFunction function to find PackageStateInternal for given package + * @return QueryIntentActivitiesResult which contains resolveInfos + */ + public QueryIntentActivitiesResult combineFilterAndCreateQueryActivitiesResponse( + Computer computer, Intent intent, String resolvedType, String instantAppPkgName, + String pkgName, boolean allowDynamicSplits, long matchFlags, int userId, + int filterCallingUid, boolean resolveForStart, List candidates, + List crossProfileCandidates, boolean areWebInstantAppsDisabled, + boolean addInstant, boolean sortResult, + Function pkgSettingFunction) { + + if (shouldSkipCurrentProfile(computer, intent, resolvedType, userId)) { + /* + if current profile is skipped return results from cross profile after filtering + ephemeral activities. + */ + candidates = resolveInfoFromCrossProfileDomainInfo(crossProfileCandidates); + + return new QueryIntentActivitiesResult(computer.applyPostResolutionFilter(candidates, + instantAppPkgName, allowDynamicSplits, filterCallingUid, resolveForStart, + userId, intent)); + } + + if (pkgName == null && intent.hasWebURI()) { + // If instant apps are not allowed and there is result only from current or cross + // profile return it + if (!addInstant && ((candidates.size() <= 1 && crossProfileCandidates.isEmpty()) + || (candidates.isEmpty() && !crossProfileCandidates.isEmpty()))) { + candidates.addAll(resolveInfoFromCrossProfileDomainInfo(crossProfileCandidates)); + return new QueryIntentActivitiesResult(computer.applyPostResolutionFilter( + candidates, instantAppPkgName, allowDynamicSplits, filterCallingUid, + resolveForStart, userId, intent)); + } + /* + if there are multiple results from current and cross profile, combining and filtering + results based on domain priority. + */ + candidates = filterCandidatesWithDomainPreferredActivitiesLPr(computer, intent, + matchFlags, candidates, crossProfileCandidates, userId, + areWebInstantAppsDisabled, pkgSettingFunction); + } else { + candidates.addAll(resolveInfoFromCrossProfileDomainInfo(crossProfileCandidates)); + } + + return new QueryIntentActivitiesResult(sortResult, addInstant, candidates); + } + + /** + * It filters and combines results from current and cross profile based on domain priority. + * @param computer {@link Computer} instance + * @param intent request + * @param matchFlags flags for intent request + * @param candidates resolveInfos from current profile + * @param crossProfileCandidates crossProfileDomainInfos from cross profile, it have ResolveInfo + * @param userId user id of source user + * @param areWebInstantAppsDisabled true if web instant apps are disabled + * @param pkgSettingFunction function to find PackageStateInternal for given package + * @return list of ResolveInfo + */ + private List filterCandidatesWithDomainPreferredActivitiesLPr(Computer computer, + Intent intent, long matchFlags, List candidates, + List crossProfileCandidates, int userId, + boolean areWebInstantAppsDisabled, + Function pkgSettingFunction) { + final boolean debug = (intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0; + + if (DEBUG_PREFERRED || DEBUG_DOMAIN_VERIFICATION) { + Slog.v(TAG, "Filtering results with preferred activities. Candidates count: " + + candidates.size()); + } + + final List result = + filterCandidatesWithDomainPreferredActivitiesLPrBody(computer, intent, matchFlags, + candidates, crossProfileCandidates, userId, areWebInstantAppsDisabled, + debug, pkgSettingFunction); + + if (DEBUG_PREFERRED || DEBUG_DOMAIN_VERIFICATION) { + Slog.v(TAG, "Filtered results with preferred activities. New candidates count: " + + result.size()); + for (ResolveInfo info : result) { + Slog.v(TAG, " + " + info.activityInfo); + } + } + return result; + } + + /** + * Filters candidates satisfying domain criteria. + * @param computer {@link Computer} instance + * @param intent request + * @param matchFlags flags for intent request + * @param candidates resolveInfos from current profile + * @param crossProfileCandidates crossProfileDomainInfos from cross profile, it have ResolveInfo + * @param userId user id of source user + * @param areWebInstantAppsDisabled true if web instant apps are disabled + * @param debug true if resolution logs needed to be printed + * @param pkgSettingFunction function to find PackageStateInternal for given package + * @return list of resolve infos + */ + private List filterCandidatesWithDomainPreferredActivitiesLPrBody( + Computer computer, Intent intent, long matchFlags, List candidates, + List crossProfileCandidates, int userId, + boolean areWebInstantAppsDisabled, boolean debug, + Function pkgSettingFunction) { + final ArrayList result = new ArrayList<>(); + final ArrayList matchAllList = new ArrayList<>(); + final ArrayList undefinedList = new ArrayList<>(); + + // Blocking instant apps is usually done in applyPostResolutionFilter, but since + // domain verification can resolve to a single result, which can be an instant app, + // it will then be filtered to an empty list in that method. Instead, do blocking + // here so that instant apps can be ignored for approval filtering and a lower + // priority result chosen instead. + final boolean blockInstant = intent.isWebIntent() && areWebInstantAppsDisabled; + + final int count = candidates.size(); + // First, try to use approved apps. + for (int n = 0; n < count; n++) { + ResolveInfo info = candidates.get(n); + if (blockInstant && (info.isInstantAppAvailable + || computer.isInstantAppInternal(info.activityInfo.packageName, userId, + Process.SYSTEM_UID))) { + continue; + } + + // Add to the special match all list (Browser use case) + if (info.handleAllWebDataURI) { + matchAllList.add(info); + } else { + undefinedList.add(info); + } + } + + // We'll want to include browser possibilities in a few cases + boolean includeBrowser = false; + + /** + * Grouping CrossProfileDomainInfo based on target user + */ + SparseArray> categorizeResolveInfoByTargetUser = + new SparseArray<>(); + if (crossProfileCandidates != null && !crossProfileCandidates.isEmpty()) { + for (int index = 0; index < crossProfileCandidates.size(); index++) { + CrossProfileDomainInfo crossProfileDomainInfo = crossProfileCandidates.get(index); + if (!categorizeResolveInfoByTargetUser + .contains(crossProfileDomainInfo.mTargetUserId)) { + categorizeResolveInfoByTargetUser.put(crossProfileDomainInfo.mTargetUserId, + new ArrayList<>()); + } + categorizeResolveInfoByTargetUser.get(crossProfileDomainInfo.mTargetUserId) + .add(crossProfileDomainInfo); + } + } + + if (!DomainVerificationUtils.isDomainVerificationIntent(intent, matchFlags)) { + result.addAll(undefinedList); + + // calling cross profile strategy to filter corresponding results + result.addAll(filterCrossProfileCandidatesWithDomainPreferredActivities(computer, + intent, matchFlags, categorizeResolveInfoByTargetUser, userId, + DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE)); + includeBrowser = true; + } else { + Pair, Integer> infosAndLevel = mDomainVerificationManager + .filterToApprovedApp(intent, undefinedList, userId, pkgSettingFunction); + List approvedInfos = infosAndLevel.first; + Integer highestApproval = infosAndLevel.second; + + // If no apps are approved for the domain, resolve only to browsers + if (approvedInfos.isEmpty()) { + includeBrowser = true; + // calling cross profile strategy to filter corresponding results + result.addAll(filterCrossProfileCandidatesWithDomainPreferredActivities(computer, + intent, matchFlags, categorizeResolveInfoByTargetUser, userId, + DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE)); + } else { + result.addAll(approvedInfos); + + // If the other profile has an app that's higher approval, add it + // calling cross profile strategy to filter corresponding results + result.addAll(filterCrossProfileCandidatesWithDomainPreferredActivities(computer, + intent, matchFlags, categorizeResolveInfoByTargetUser, userId, + highestApproval)); + } + } + + if (includeBrowser) { + // Also add browsers (all of them or only the default one) + if (DEBUG_DOMAIN_VERIFICATION) { + Slog.v(TAG, " ...including browsers in candidate set"); + } + if ((matchFlags & MATCH_ALL) != 0) { + result.addAll(matchAllList); + } else { + // Browser/generic handling case. If there's a default browser, go straight + // to that (but only if there is no other higher-priority match). + final String defaultBrowserPackageName = mDefaultAppProvider.getDefaultBrowser( + userId); + int maxMatchPrio = 0; + ResolveInfo defaultBrowserMatch = null; + final int numCandidates = matchAllList.size(); + for (int n = 0; n < numCandidates; n++) { + ResolveInfo info = matchAllList.get(n); + // track the highest overall match priority... + if (info.priority > maxMatchPrio) { + maxMatchPrio = info.priority; + } + // ...and the highest-priority default browser match + if (info.activityInfo.packageName.equals(defaultBrowserPackageName)) { + if (defaultBrowserMatch == null + || (defaultBrowserMatch.priority < info.priority)) { + if (debug) { + Slog.v(TAG, "Considering default browser match " + info); + } + defaultBrowserMatch = info; + } + } + } + if (defaultBrowserMatch != null + && defaultBrowserMatch.priority >= maxMatchPrio + && !TextUtils.isEmpty(defaultBrowserPackageName)) { + if (debug) { + Slog.v(TAG, "Default browser match " + defaultBrowserMatch); + } + result.add(defaultBrowserMatch); + } else { + result.addAll(matchAllList); + } + } + + // If there is nothing selected, add all candidates + if (result.size() == 0) { + result.addAll(candidates); + } + } + return result; + } + + /** + * Filter cross profile results by calling their respective strategy + * @param computer {@link Computer} instance + * @param intent request + * @param flags for intent request + * @param categorizeResolveInfoByTargetUser group of targetuser and its corresponding + * CrossProfileDomainInfos + * @param sourceUserId user id for intent + * @param highestApprovalLevel domain approval level + * @return list of ResolveInfos + */ + private List filterCrossProfileCandidatesWithDomainPreferredActivities( + Computer computer, Intent intent, long flags, SparseArray> + categorizeResolveInfoByTargetUser, int sourceUserId, int highestApprovalLevel) { + + List crossProfileDomainInfos = new ArrayList<>(); + UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class); + UserInfo sourceUserInfo = umInternal.getUserInfo(sourceUserId); + + for (int index = 0; index < categorizeResolveInfoByTargetUser.size(); index++) { + + // if resolve info does not target user or has default value, add results as they are. + if (categorizeResolveInfoByTargetUser.keyAt(index) == -2) { + crossProfileDomainInfos.addAll(categorizeResolveInfoByTargetUser.valueAt(index)); + } else { + // finding cross profile strategy based on source and target user + CrossProfileResolver crossProfileIntentResolver = + chooseCrossProfileResolver(computer, sourceUserInfo, umInternal + .getUserInfo(categorizeResolveInfoByTargetUser.keyAt(index))); + // if strategy is available call it and add its filtered results + if (crossProfileIntentResolver != null) { + crossProfileDomainInfos.addAll(crossProfileIntentResolver + .filterResolveInfoWithDomainPreferredActivity(intent, + categorizeResolveInfoByTargetUser.valueAt(index), + flags, sourceUserId, categorizeResolveInfoByTargetUser + .keyAt(index), highestApprovalLevel)); + } else { + // if strategy is not available call it, add the results + crossProfileDomainInfos.addAll(categorizeResolveInfoByTargetUser + .valueAt(index)); + } + } + } + return resolveInfoFromCrossProfileDomainInfo(crossProfileDomainInfos); + } + + /** + * Extract ResolveInfo from CrossProfileDomainInfo + * @param crossProfileDomainInfos cross profile results + * @return list of ResolveInfo + */ + private List resolveInfoFromCrossProfileDomainInfo(List + crossProfileDomainInfos) { + List resolveInfoList = new ArrayList<>(); + + for (int infoIndex = 0; infoIndex < crossProfileDomainInfos.size(); infoIndex++) { + resolveInfoList.add(crossProfileDomainInfos.get(infoIndex).mResolveInfo); + } + + return resolveInfoList; + } +} diff --git a/services/core/java/com/android/server/pm/CrossProfileResolver.java b/services/core/java/com/android/server/pm/CrossProfileResolver.java new file mode 100644 index 0000000000000..a8da818932fa0 --- /dev/null +++ b/services/core/java/com/android/server/pm/CrossProfileResolver.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ResolveInfo; +import android.content.pm.UserInfo; +import android.os.Binder; +import android.os.UserHandle; + +import com.android.internal.util.CollectionUtils; +import com.android.server.pm.pkg.PackageStateInternal; +import com.android.server.pm.resolution.ComponentResolverApi; + +import java.util.List; +import java.util.function.Function; + +/** + * Abstract Class act as base class for Cross Profile strategy. + * This will be used by {@link CrossProfileIntentResolverEngine} to resolve intent across profile. + */ +public abstract class CrossProfileResolver { + + protected ComponentResolverApi mComponentResolver; + protected UserManagerService mUserManager; + + public CrossProfileResolver(ComponentResolverApi componentResolver, + UserManagerService userManager) { + mComponentResolver = componentResolver; + mUserManager = userManager; + } + + /** + * This method would be overridden by concrete implementation. This method should define how to + * resolve given intent request in target profile. + * @param computer ComputerEngine instance that would be needed by ComponentResolverApi + * @param intent request + * @param resolvedType the MIME data type of intent request + * @param userId source/initiating user + * @param targetUserId target user id + * @param flags of intent request + * @param pkgName package name if defined. + * @param matchingFilters {@link CrossProfileIntentFilter}s configured for source user, + * targeting the targetUserId + * @param hasNonNegativePriorityResult if source have any non-negative(active and valid) + * resolveInfo in their profile. + * @param pkgSettingFunction function to find PackageStateInternal for given package + * @return list of {@link CrossProfileDomainInfo} + */ + public abstract List resolveIntent(Computer computer, Intent intent, + String resolvedType, int userId, int targetUserId, long flags, + String pkgName, List matchingFilters, + boolean hasNonNegativePriorityResult, + Function pkgSettingFunction); + + /** + * Filters the CrossProfileDomainInfos, the filtering technique would be defined by concrete + * implementation class + * @param intent request + * @param crossProfileDomainInfos resolved in target user + * @param flags for intent resolution + * @param sourceUserId source user + * @param targetUserId target user + * @param highestApprovalLevel highest level of domain approval + * @return filtered list of {@link CrossProfileDomainInfo} + */ + public abstract List filterResolveInfoWithDomainPreferredActivity( + Intent intent, List crossProfileDomainInfos, long flags, + int sourceUserId, int targetUserId, int highestApprovalLevel); + + /** + * Checks if mentioned user is enabled + * @param userId of requested user + * @return true if user is enabled + */ + protected final boolean isUserEnabled(int userId) { + final long callingId = Binder.clearCallingIdentity(); + try { + UserInfo userInfo = mUserManager.getUserInfo(userId); + return userInfo != null && userInfo.isEnabled(); + } finally { + Binder.restoreCallingIdentity(callingId); + } + } + + /** + * Filters out {@link CrossProfileDomainInfo} if they are not for any user apart from system + * user. If mentioned user is system user, then returns all responses. + * @param crossProfileDomainInfos result from resolution + * @param userId source user id + * @return filtered list of {@link CrossProfileDomainInfo} + */ + protected final List filterIfNotSystemUser( + List crossProfileDomainInfos, int userId) { + if (userId == UserHandle.USER_SYSTEM) { + return crossProfileDomainInfos; + } + + for (int i = CollectionUtils.size(crossProfileDomainInfos) - 1; i >= 0; i--) { + ResolveInfo info = crossProfileDomainInfos.get(i).mResolveInfo; + if ((info.activityInfo.flags & ActivityInfo.FLAG_SYSTEM_USER_ONLY) != 0) { + crossProfileDomainInfos.remove(i); + } + } + return crossProfileDomainInfos; + } + + /** + * Returns user info of parent profile is applicable + * @param userId requested user + * @return parent's user info, null if parent is not present + */ + protected final UserInfo getProfileParent(int userId) { + final long identity = Binder.clearCallingIdentity(); + try { + return mUserManager.getProfileParent(userId); + } finally { + Binder.restoreCallingIdentity(identity); + } + } +} diff --git a/services/core/java/com/android/server/pm/DefaultCrossProfileResolver.java b/services/core/java/com/android/server/pm/DefaultCrossProfileResolver.java new file mode 100644 index 0000000000000..90d89c6066863 --- /dev/null +++ b/services/core/java/com/android/server/pm/DefaultCrossProfileResolver.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.UserInfo; +import android.util.SparseBooleanArray; + +import com.android.internal.app.IntentForwarderActivity; +import com.android.internal.util.CollectionUtils; +import com.android.server.pm.pkg.PackageStateInternal; +import com.android.server.pm.resolution.ComponentResolverApi; +import com.android.server.pm.verify.domain.DomainVerificationManagerInternal; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +/** + * Cross profile resolver used as default strategy. Primary known use-case for this resolver is + * work/managed profile . + */ +public final class DefaultCrossProfileResolver extends CrossProfileResolver { + + private final DomainVerificationManagerInternal mDomainVerificationManager; + + + public DefaultCrossProfileResolver(ComponentResolverApi componentResolver, + UserManagerService userManager, + DomainVerificationManagerInternal domainVerificationManager) { + super(componentResolver, userManager); + mDomainVerificationManager = domainVerificationManager; + } + + /** + * This is Default resolution strategy primarily used by Work Profile. + * First, it checks if we have to skip source profile and just resolve in target profile. If + * yes, then it will return result from target profile. + * Secondly, it find specific resolve infos in target profile + * Thirdly, if it is web intent it finds if parent can also resolve it. The results of this + * stage gets higher priority as compared to second stage. + * + * @param computer ComputerEngine instance that would be needed by ComponentResolverApi + * @param intent request + * @param resolvedType the MIME data type of intent request + * @param userId source/initiating user + * @param targetUserId target user id + * @param flags of intent request + * @param pkgName the application package name this Intent is limited to. + * @param matchingFilters {@link CrossProfileIntentFilter}s configured for source user, + * targeting the targetUserId + * @param hasNonNegativePriorityResult if source have any non-negative(active and valid) + * resolveInfo in their profile. + * @param pkgSettingFunction function to find PackageStateInternal for given package + * @return list of {@link CrossProfileDomainInfo} + */ + @Override + public List resolveIntent(Computer computer, Intent intent, + String resolvedType, int userId, int targetUserId, + long flags, String pkgName, List matchingFilters, + boolean hasNonNegativePriorityResult, + Function pkgSettingFunction) { + + List xpResult = new ArrayList<>(); + if (pkgName != null) return xpResult; + CrossProfileDomainInfo skipProfileInfo = querySkipCurrentProfileIntents(computer, + matchingFilters, intent, resolvedType, flags, userId, pkgSettingFunction); + + if (skipProfileInfo != null) { + xpResult.add(skipProfileInfo); + return filterIfNotSystemUser(xpResult, userId); + } + + CrossProfileDomainInfo specificXpInfo = queryCrossProfileIntents(computer, + matchingFilters, intent, resolvedType, flags, userId, + hasNonNegativePriorityResult, pkgSettingFunction); + + if (intent.hasWebURI()) { + CrossProfileDomainInfo generalXpInfo = null; + final UserInfo parent = getProfileParent(userId); + if (parent != null) { + generalXpInfo = computer.getCrossProfileDomainPreferredLpr(intent, resolvedType, + flags, userId, parent.id); + } + CrossProfileDomainInfo prioritizedXpInfo = + generalXpInfo != null ? generalXpInfo : specificXpInfo; + if (prioritizedXpInfo != null) { + xpResult.add(prioritizedXpInfo); + } + } else if (specificXpInfo != null) { + xpResult.add(specificXpInfo); + } + + return xpResult; + } + + /** + * Filters out CrossProfileDomainInfo if it does not have higher approval level as compared to + * given approval level + * @param intent request + * @param crossProfileDomainInfos resolved in target user + * @param flags for intent resolution + * @param sourceUserId source user + * @param targetUserId target user + * @param highestApprovalLevel highest level of domain approval + * @return filtered list of CrossProfileDomainInfo + */ + @Override + public List filterResolveInfoWithDomainPreferredActivity( + Intent intent, List crossProfileDomainInfos, long flags, + int sourceUserId, int targetUserId, int highestApprovalLevel) { + + List filteredCrossProfileDomainInfos = new ArrayList<>(); + + if (crossProfileDomainInfos != null && !crossProfileDomainInfos.isEmpty()) { + for (int index = 0; index < crossProfileDomainInfos.size(); index++) { + CrossProfileDomainInfo crossProfileDomainInfo = crossProfileDomainInfos.get(index); + if (crossProfileDomainInfo.mHighestApprovalLevel > highestApprovalLevel) { + filteredCrossProfileDomainInfos.add(crossProfileDomainInfo); + } + } + } + + return filteredCrossProfileDomainInfos; + } + + /** + * If current/source profile needs to be skipped, returns CrossProfileDomainInfo from target + * profile. If any of the matchingFilters have flag {@link PackageManager#SKIP_CURRENT_PROFILE} + * set that would signify that current profile needs to be skipped. + * @param computer ComputerEngine instance that would be needed by ComponentResolverApi + * @param matchingFilters {@link CrossProfileIntentFilter}s configured for source user, + * targeting the targetUserId + * @param intent request + * @param resolvedType the MIME data type of intent request + * @param flags for intent resolution + * @param sourceUserId source user + * @param pkgSettingFunction function to find PackageStateInternal for given package + * @return CrossProfileDomainInfo if current profile needs to be skipped, else null + */ + @Nullable + private CrossProfileDomainInfo querySkipCurrentProfileIntents(Computer computer, + List matchingFilters, Intent intent, String resolvedType, + long flags, int sourceUserId, + Function pkgSettingFunction) { + if (matchingFilters != null) { + int size = matchingFilters.size(); + for (int i = 0; i < size; i++) { + CrossProfileIntentFilter filter = matchingFilters.get(i); + if ((filter.getFlags() & PackageManager.SKIP_CURRENT_PROFILE) != 0) { + // Checking if there are activities in the target user that can handle the + // intent. + CrossProfileDomainInfo info = createForwardingResolveInfo(computer, filter, + intent, resolvedType, flags, sourceUserId, pkgSettingFunction); + if (info != null) { + return info; + } + } + } + } + return null; + } + + /** + * Resolves and returns CrossProfileDomainInfo(ForwardingResolveInfo) from target profile if + * current profile should be skipped when there is no result or if target profile should not + * be skipped. + * + * @param computer ComputerEngine instance that would be needed by ComponentResolverApi + * @param matchingFilters {@link CrossProfileIntentFilter}s configured for source user, + * targeting the targetUserId + * @param intent request + * @param resolvedType the MIME data type of intent request + * @param flags for intent resolution + * @param sourceUserId source user + * @param matchInCurrentProfile true if current/source profile have some non-negative + * resolveInfo + * @param pkgSettingFunction function to find PackageStateInternal for given package + * @return CrossProfileDomainInfo returns forwarding intent resolver in CrossProfileDomainInfo. + * It returns null if there are no matching filters or no valid/active activity available + */ + @Nullable + private CrossProfileDomainInfo queryCrossProfileIntents(Computer computer, + List matchingFilters, Intent intent, String resolvedType, + long flags, int sourceUserId, boolean matchInCurrentProfile, + Function pkgSettingFunction) { + if (matchingFilters == null) { + return null; + } + // Two {@link CrossProfileIntentFilter}s can have the same targetUserId and + // match the same intent. For performance reasons, it is better not to + // run queryIntent twice for the same userId + SparseBooleanArray alreadyTriedUserIds = new SparseBooleanArray(); + + CrossProfileDomainInfo resultInfo = null; + + int size = matchingFilters.size(); + for (int i = 0; i < size; i++) { + CrossProfileIntentFilter filter = matchingFilters.get(i); + int targetUserId = filter.getTargetUserId(); + boolean skipCurrentProfile = + (filter.getFlags() & PackageManager.SKIP_CURRENT_PROFILE) != 0; + boolean skipCurrentProfileIfNoMatchFound = + (filter.getFlags() & PackageManager.ONLY_IF_NO_MATCH_FOUND) != 0; + if (!skipCurrentProfile && !alreadyTriedUserIds.get(targetUserId) + && (!skipCurrentProfileIfNoMatchFound || !matchInCurrentProfile)) { + // Checking if there are activities in the target user that can handle the + // intent. + CrossProfileDomainInfo info = createForwardingResolveInfo(computer, filter, intent, + resolvedType, flags, sourceUserId, pkgSettingFunction); + if (info != null) { + resultInfo = info; + break; + } + alreadyTriedUserIds.put(targetUserId, true); + } + } + + if (resultInfo == null) { + return null; + } + + ResolveInfo forwardingResolveInfo = resultInfo.mResolveInfo; + if (!isUserEnabled(forwardingResolveInfo.targetUserId)) { + return null; + } + + List filteredResult = + filterIfNotSystemUser(Collections.singletonList(resultInfo), sourceUserId); + if (filteredResult.isEmpty()) { + return null; + } + + return resultInfo; + } + + /** + * Creates a Forwarding Resolve Info, used when we have to signify that target profile's + * resolveInfo should be considered without providing list of resolve infos. + * @param computer ComputerEngine instance that would be needed by ComponentResolverApi + * @param filter {@link CrossProfileIntentFilter} configured for source user, + * targeting the targetUserId + * @param intent request + * @param resolvedType the MIME data type of intent request + * @param flags for intent resolution + * @param sourceUserId source user + * @return CrossProfileDomainInfo whose ResolveInfo is forwarding. It would be resolved by + * {@link IntentForwarderActivity}. It returns null if there are no valid/active activities + */ + @Nullable + protected CrossProfileDomainInfo createForwardingResolveInfo(Computer computer, + @NonNull CrossProfileIntentFilter filter, @NonNull Intent intent, + @Nullable String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags, + int sourceUserId, @NonNull Function pkgSettingFunction) { + int targetUserId = filter.getTargetUserId(); + if (!isUserEnabled(targetUserId)) { + return null; + } + + List resultTargetUser = mComponentResolver.queryActivities(computer, intent, + resolvedType, flags, targetUserId); + if (CollectionUtils.isEmpty(resultTargetUser)) { + return null; + } + + ResolveInfo forwardingInfo = null; + for (int i = resultTargetUser.size() - 1; i >= 0; i--) { + ResolveInfo targetUserResolveInfo = resultTargetUser.get(i); + if ((targetUserResolveInfo.activityInfo.applicationInfo.flags + & ApplicationInfo.FLAG_SUSPENDED) == 0) { + forwardingInfo = computer.createForwardingResolveInfoUnchecked(filter, sourceUserId, + targetUserId); + break; + } + } + + if (forwardingInfo == null) { + // If all the matches in the target profile are suspended, return null. + return null; + } + + int highestApprovalLevel = DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE; + + int size = resultTargetUser.size(); + for (int i = 0; i < size; i++) { + ResolveInfo riTargetUser = resultTargetUser.get(i); + if (riTargetUser.handleAllWebDataURI) { + continue; + } + String packageName = riTargetUser.activityInfo.packageName; + PackageStateInternal ps = pkgSettingFunction.apply(packageName); + if (ps == null) { + continue; + } + highestApprovalLevel = Math.max(highestApprovalLevel, mDomainVerificationManager + .approvalLevelForDomain(ps, intent, flags, targetUserId)); + } + + return new CrossProfileDomainInfo(forwardingInfo, highestApprovalLevel, targetUserId); + } +}