From 68250e3be70f5d1206b0cb0855f922904d1e8ca0 Mon Sep 17 00:00:00 2001 From: Sarup Dalwani Date: Wed, 17 Aug 2022 18:49:04 +0000 Subject: [PATCH] Creating CrossProfileResolverEngine which would facilitate all cross profile intent resolution Creating CrossProfileResolverEngine which would be general entry point for all the cross profile intent resolution. This would also hold strategy resolution engine to determine which strategy to use for resolution based on source,target users. Moving cross profile logic from ComputerEngine to CrossProfileResolverEngine, this include most of the logic for WorkProfile. Bug: 242885222 Test: atest CtsDevicePolicyManagerTestCases:ManagedProfileTest Test: atest CtsDevicePolicyManagerTestCases:ManagedProfileCrossProfileTest Test: Manually tested with work profile Change-Id: I4ab1c4b52235fec6a5b9a17454318f5b4fc63869 --- .../java/com/android/server/pm/Computer.java | 2 +- .../com/android/server/pm/ComputerEngine.java | 432 ++----------- .../server/pm/CrossProfileDomainInfo.java | 12 + .../pm/CrossProfileIntentResolverEngine.java | 575 ++++++++++++++++++ .../server/pm/CrossProfileResolver.java | 136 +++++ .../pm/DefaultCrossProfileResolver.java | 321 ++++++++++ 6 files changed, 1101 insertions(+), 377 deletions(-) create mode 100644 services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java create mode 100644 services/core/java/com/android/server/pm/CrossProfileResolver.java create mode 100644 services/core/java/com/android/server/pm/DefaultCrossProfileResolver.java 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); + } +}