Merge "Creating CrossProfileResolverEngine which would facilitate all cross profile intent resolution"

This commit is contained in:
Sarup Dalwani
2022-10-13 06:29:02 +00:00
committed by Android (Google) Code Review
6 changed files with 1101 additions and 377 deletions

View File

@@ -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();

View File

@@ -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<ResolveInfo> result = null;
List<ResolveInfo> result = new ArrayList<>();
// crossProfileResults will hold resolve infos from resolution across profiles.
List<CrossProfileDomainInfo> crossProfileResults = new ArrayList<>();
if (pkgName == null) {
List<CrossProfileIntentFilter> 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<ResolveInfo> 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<ResolveInfo> filterCandidatesWithDomainPreferredActivitiesLPrBody(
Intent intent, long matchFlags, List<ResolveInfo> candidates,
CrossProfileDomainInfo xpDomainInfo, int userId, boolean debug) {
final ArrayList<ResolveInfo> result = new ArrayList<>();
final ArrayList<ResolveInfo> matchAllList = new ArrayList<>();
final ArrayList<ResolveInfo> 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<List<ResolveInfo>, Integer> infosAndLevel = mDomainVerificationManager
.filterToApprovedApp(intent, undefinedList, userId,
mSettings::getPackage);
List<ResolveInfo> 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<ResolveInfo> filterCandidatesWithDomainPreferredActivitiesLPr(Intent intent,
long matchFlags, List<ResolveInfo> 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<ResolveInfo> 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<ResolveInfo> 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<CrossProfileIntentFilter> 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<ResolveInfo> filteredResult =
filterIfNotSystemUser(Collections.singletonList(forwardingResolveInfo),
sourceUserId);
if (filteredResult.isEmpty()) {
return null;
}
return resultInfo;
}
private ResolveInfo querySkipCurrentProfileIntents(
List<CrossProfileIntentFilter> 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.
* <p>
@@ -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<CrossProfileIntentFilter> 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.

View File

@@ -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
+ '}';
}
}

View File

@@ -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<CrossProfileDomainInfo> resolveIntent(@NonNull Computer computer, Intent intent,
String resolvedType, int userId, long flags, String pkgName,
boolean hasNonNegativePriorityResult,
Function<String, PackageStateInternal> 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<CrossProfileDomainInfo> resolveIntentInternal(@NonNull Computer computer,
Intent intent, String resolvedType, int userId, long flags, String pkgName,
boolean hasNonNegativePriorityResult,
Function<String, PackageStateInternal> pkgSettingFunction) {
List<CrossProfileDomainInfo> crossProfileDomainInfos = new ArrayList<>();
List<CrossProfileIntentFilter> 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<List<CrossProfileIntentFilter>> 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<CrossProfileDomainInfo> 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<CrossProfileIntentFilter> 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<CrossProfileIntentFilter> 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<ResolveInfo> candidates,
List<CrossProfileDomainInfo> crossProfileCandidates, boolean areWebInstantAppsDisabled,
boolean addInstant, boolean sortResult,
Function<String, PackageStateInternal> 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<ResolveInfo> filterCandidatesWithDomainPreferredActivitiesLPr(Computer computer,
Intent intent, long matchFlags, List<ResolveInfo> candidates,
List<CrossProfileDomainInfo> crossProfileCandidates, int userId,
boolean areWebInstantAppsDisabled,
Function<String, PackageStateInternal> 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<ResolveInfo> 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<ResolveInfo> filterCandidatesWithDomainPreferredActivitiesLPrBody(
Computer computer, Intent intent, long matchFlags, List<ResolveInfo> candidates,
List<CrossProfileDomainInfo> crossProfileCandidates, int userId,
boolean areWebInstantAppsDisabled, boolean debug,
Function<String, PackageStateInternal> pkgSettingFunction) {
final ArrayList<ResolveInfo> result = new ArrayList<>();
final ArrayList<ResolveInfo> matchAllList = new ArrayList<>();
final ArrayList<ResolveInfo> 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<List<CrossProfileDomainInfo>> 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<List<ResolveInfo>, Integer> infosAndLevel = mDomainVerificationManager
.filterToApprovedApp(intent, undefinedList, userId, pkgSettingFunction);
List<ResolveInfo> 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<ResolveInfo> filterCrossProfileCandidatesWithDomainPreferredActivities(
Computer computer, Intent intent, long flags, SparseArray<List<CrossProfileDomainInfo>>
categorizeResolveInfoByTargetUser, int sourceUserId, int highestApprovalLevel) {
List<CrossProfileDomainInfo> 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<ResolveInfo> resolveInfoFromCrossProfileDomainInfo(List<CrossProfileDomainInfo>
crossProfileDomainInfos) {
List<ResolveInfo> resolveInfoList = new ArrayList<>();
for (int infoIndex = 0; infoIndex < crossProfileDomainInfos.size(); infoIndex++) {
resolveInfoList.add(crossProfileDomainInfos.get(infoIndex).mResolveInfo);
}
return resolveInfoList;
}
}

View File

@@ -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<CrossProfileDomainInfo> resolveIntent(Computer computer, Intent intent,
String resolvedType, int userId, int targetUserId, long flags,
String pkgName, List<CrossProfileIntentFilter> matchingFilters,
boolean hasNonNegativePriorityResult,
Function<String, PackageStateInternal> 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<CrossProfileDomainInfo> filterResolveInfoWithDomainPreferredActivity(
Intent intent, List<CrossProfileDomainInfo> 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<CrossProfileDomainInfo> filterIfNotSystemUser(
List<CrossProfileDomainInfo> 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);
}
}
}

View File

@@ -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<CrossProfileDomainInfo> resolveIntent(Computer computer, Intent intent,
String resolvedType, int userId, int targetUserId,
long flags, String pkgName, List<CrossProfileIntentFilter> matchingFilters,
boolean hasNonNegativePriorityResult,
Function<String, PackageStateInternal> pkgSettingFunction) {
List<CrossProfileDomainInfo> 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<CrossProfileDomainInfo> filterResolveInfoWithDomainPreferredActivity(
Intent intent, List<CrossProfileDomainInfo> crossProfileDomainInfos, long flags,
int sourceUserId, int targetUserId, int highestApprovalLevel) {
List<CrossProfileDomainInfo> 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<CrossProfileIntentFilter> matchingFilters, Intent intent, String resolvedType,
long flags, int sourceUserId,
Function<String, PackageStateInternal> 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<CrossProfileIntentFilter> matchingFilters, Intent intent, String resolvedType,
long flags, int sourceUserId, boolean matchInCurrentProfile,
Function<String, PackageStateInternal> 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<CrossProfileDomainInfo> 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<String, PackageStateInternal> pkgSettingFunction) {
int targetUserId = filter.getTargetUserId();
if (!isUserEnabled(targetUserId)) {
return null;
}
List<ResolveInfo> 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);
}
}