| /* |
| * 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); |
| } |
| } |