diff options
5 files changed, 154 insertions, 124 deletions
diff --git a/core/java/android/content/pm/ResolveInfo.java b/core/java/android/content/pm/ResolveInfo.java index fe8e4d7b1bb2..6f07dd7a24e8 100644 --- a/core/java/android/content/pm/ResolveInfo.java +++ b/core/java/android/content/pm/ResolveInfo.java @@ -19,6 +19,7 @@ package android.content.pm; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; +import android.content.Intent; import android.content.IntentFilter; import android.graphics.drawable.Drawable; import android.os.Build; @@ -183,6 +184,17 @@ public class ResolveInfo implements Parcelable { @SystemApi public boolean handleAllWebDataURI; + /** + * Whether the resolved {@link IntentFilter} declares {@link Intent#CATEGORY_BROWSABLE} and is + * thus allowed to automatically resolve an {@link Intent} as it's assumed the action is safe + * for the user. + * + * Note that the above doesn't apply when this is the only result is returned in the candidate + * set, as the system will not prompt before opening the result. It only applies when there are + * multiple candidates. + */ + private final boolean mAutoResolutionAllowed; + /** {@hide} */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public ComponentInfo getComponentInfo() { @@ -364,8 +376,26 @@ public class ResolveInfo implements Parcelable { && INTENT_FORWARDER_ACTIVITY.equals(activityInfo.targetActivity); } + /** + * @see #mAutoResolutionAllowed + * @hide + */ + public boolean isAutoResolutionAllowed() { + return mAutoResolutionAllowed; + } + public ResolveInfo() { targetUserId = UserHandle.USER_CURRENT; + + // It's safer to assume that an unaware caller that constructs a ResolveInfo doesn't + // accidentally mark a result as auto resolveable. + mAutoResolutionAllowed = false; + } + + /** @hide */ + public ResolveInfo(boolean autoResolutionAllowed) { + targetUserId = UserHandle.USER_CURRENT; + mAutoResolutionAllowed = autoResolutionAllowed; } public ResolveInfo(ResolveInfo orig) { @@ -386,6 +416,7 @@ public class ResolveInfo implements Parcelable { system = orig.system; targetUserId = orig.targetUserId; handleAllWebDataURI = orig.handleAllWebDataURI; + mAutoResolutionAllowed = orig.mAutoResolutionAllowed; isInstantAppAvailable = orig.isInstantAppAvailable; } @@ -450,6 +481,7 @@ public class ResolveInfo implements Parcelable { dest.writeInt(noResourceId ? 1 : 0); dest.writeInt(iconResourceId); dest.writeInt(handleAllWebDataURI ? 1 : 0); + dest.writeInt(mAutoResolutionAllowed ? 1 : 0); dest.writeInt(isInstantAppAvailable ? 1 : 0); } @@ -498,6 +530,7 @@ public class ResolveInfo implements Parcelable { noResourceId = source.readInt() != 0; iconResourceId = source.readInt(); handleAllWebDataURI = source.readInt() != 0; + mAutoResolutionAllowed = source.readInt() != 0; isInstantAppAvailable = source.readInt() != 0; } diff --git a/services/core/java/com/android/server/pm/ComponentResolver.java b/services/core/java/com/android/server/pm/ComponentResolver.java index 4be509b3f464..1d556fec31ea 100644 --- a/services/core/java/com/android/server/pm/ComponentResolver.java +++ b/services/core/java/com/android/server/pm/ComponentResolver.java @@ -1492,7 +1492,7 @@ public class ComponentResolver { } return null; } - final ResolveInfo res = new ResolveInfo(); + final ResolveInfo res = new ResolveInfo(info.hasCategory(Intent.CATEGORY_BROWSABLE)); res.activityInfo = ai; if ((mFlags & PackageManager.GET_RESOLVED_FILTER) != 0) { res.filter = info; diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 317effab3750..1530e41917c7 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -2644,8 +2644,7 @@ public class PackageManagerService extends IPackageManager.Stub // We'll want to include browser possibilities in a few cases boolean includeBrowser = false; - if (!DomainVerificationUtils.isDomainVerificationIntent(intent, candidates, - matchFlags)) { + if (!DomainVerificationUtils.isDomainVerificationIntent(intent, matchFlags)) { result.addAll(undefinedList); // Maybe add one for the other profile. if (xpDomainInfo != null && xpDomainInfo.highestApprovalLevel diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java index 76d5b7ba88b1..a0e252a8a28a 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java @@ -1330,83 +1330,50 @@ public class DomainVerificationService extends SystemService @NonNull Function<String, PackageSetting> pkgSettingFunction) { String domain = intent.getData().getHost(); - // Collect package names - ArrayMap<String, Integer> packageApprovals = new ArrayMap<>(); + // Collect valid infos + ArrayMap<ResolveInfo, Integer> infoApprovals = new ArrayMap<>(); int infosSize = infos.size(); for (int index = 0; index < infosSize; index++) { - packageApprovals.put(infos.get(index).getComponentInfo().packageName, - APPROVAL_LEVEL_NONE); + final ResolveInfo info = infos.get(index); + // Only collect for intent filters that can auto resolve + if (info.isAutoResolutionAllowed()) { + infoApprovals.put(info, null); + } } // Find all approval levels - int highestApproval = fillMapWithApprovalLevels(packageApprovals, domain, userId, + int highestApproval = fillMapWithApprovalLevels(infoApprovals, domain, userId, pkgSettingFunction); if (highestApproval == APPROVAL_LEVEL_NONE) { return Pair.create(emptyList(), highestApproval); } - // Filter to highest, non-zero packages - ArraySet<String> approvedPackages = new ArraySet<>(); - int approvalsSize = packageApprovals.size(); - for (int index = 0; index < approvalsSize; index++) { - if (packageApprovals.valueAt(index) == highestApproval) { - approvedPackages.add(packageApprovals.keyAt(index)); + // Filter to highest, non-zero infos + for (int index = infoApprovals.size() - 1; index >= 0; index--) { + if (infoApprovals.valueAt(index) != highestApproval) { + infoApprovals.removeAt(index); } } - ArraySet<String> filteredPackages = new ArraySet<>(); - if (highestApproval == APPROVAL_LEVEL_LEGACY_ASK) { + if (highestApproval != APPROVAL_LEVEL_LEGACY_ASK) { // To maintain legacy behavior while the Settings API is not implemented, // show the chooser if all approved apps are marked ask, skipping the // last app, last declaration filtering. - filteredPackages.addAll(approvedPackages); - } else { - // Filter to last installed package - long latestInstall = Long.MIN_VALUE; - int approvedSize = approvedPackages.size(); - for (int index = 0; index < approvedSize; index++) { - String packageName = approvedPackages.valueAt(index); - PackageSetting pkgSetting = pkgSettingFunction.apply(packageName); - if (pkgSetting == null) { - continue; - } - long installTime = pkgSetting.getFirstInstallTime(); - if (installTime > latestInstall) { - latestInstall = installTime; - filteredPackages.clear(); - filteredPackages.add(packageName); - } else if (installTime == latestInstall) { - filteredPackages.add(packageName); - } - } + filterToLastFirstInstalled(infoApprovals, pkgSettingFunction); } - // Filter to approved ResolveInfos - ArrayMap<String, List<ResolveInfo>> approvedInfos = new ArrayMap<>(); - for (int index = 0; index < infosSize; index++) { - ResolveInfo info = infos.get(index); - String packageName = info.getComponentInfo().packageName; - if (filteredPackages.contains(packageName)) { - List<ResolveInfo> infosPerPackage = approvedInfos.get(packageName); - if (infosPerPackage == null) { - infosPerPackage = new ArrayList<>(); - approvedInfos.put(packageName, infosPerPackage); - } - infosPerPackage.add(info); - } + // Easier to transform into list as the filterToLastDeclared method + // requires swapping indexes, which doesn't work with ArrayMap keys + final int size = infoApprovals.size(); + List<ResolveInfo> finalList = new ArrayList<>(size); + for (int index = 0; index < size; index++) { + finalList.add(infoApprovals.keyAt(index)); } - List<ResolveInfo> finalList; - if (highestApproval == APPROVAL_LEVEL_LEGACY_ASK) { - // If legacy ask, skip the last declaration filtering - finalList = new ArrayList<>(); - int size = approvedInfos.size(); - for (int index = 0; index < size; index++) { - finalList.addAll(approvedInfos.valueAt(index)); - } - } else { + // If legacy ask, skip the last declaration filtering + if (highestApproval != APPROVAL_LEVEL_LEGACY_ASK) { // Find the last declared ResolveInfo per package - finalList = filterToLastDeclared(approvedInfos, pkgSettingFunction); + filterToLastDeclared(finalList, pkgSettingFunction); } return Pair.create(finalList, highestApproval); @@ -1415,68 +1382,127 @@ public class DomainVerificationService extends SystemService /** * @return highest approval level found */ - private int fillMapWithApprovalLevels(@NonNull ArrayMap<String, Integer> inputMap, + @ApprovalLevel + private int fillMapWithApprovalLevels(@NonNull ArrayMap<ResolveInfo, Integer> inputMap, @NonNull String domain, @UserIdInt int userId, @NonNull Function<String, PackageSetting> pkgSettingFunction) { int highestApproval = APPROVAL_LEVEL_NONE; int size = inputMap.size(); for (int index = 0; index < size; index++) { - String packageName = inputMap.keyAt(index); + if (inputMap.valueAt(index) != null) { + // Already filled by previous iteration + continue; + } + + ResolveInfo info = inputMap.keyAt(index); + final String packageName = info.getComponentInfo().packageName; PackageSetting pkgSetting = pkgSettingFunction.apply(packageName); if (pkgSetting == null) { - inputMap.setValueAt(index, APPROVAL_LEVEL_NONE); + fillInfoMapForSamePackage(inputMap, packageName, APPROVAL_LEVEL_NONE); continue; } int approval = approvalLevelForDomain(pkgSetting, domain, userId, domain); highestApproval = Math.max(highestApproval, approval); - inputMap.setValueAt(index, approval); + fillInfoMapForSamePackage(inputMap, packageName, approval); } return highestApproval; } + private void fillInfoMapForSamePackage(@NonNull ArrayMap<ResolveInfo, Integer> inputMap, + @NonNull String targetPackageName, @ApprovalLevel int level) { + final int size = inputMap.size(); + for (int index = 0; index < size; index++) { + final String packageName = inputMap.keyAt(index).getComponentInfo().packageName; + if (Objects.equals(targetPackageName, packageName)) { + inputMap.setValueAt(index, level); + } + } + } + @NonNull - private List<ResolveInfo> filterToLastDeclared( - @NonNull ArrayMap<String, List<ResolveInfo>> inputMap, + private void filterToLastFirstInstalled(@NonNull ArrayMap<ResolveInfo, Integer> inputMap, @NonNull Function<String, PackageSetting> pkgSettingFunction) { - List<ResolveInfo> finalList = new ArrayList<>(inputMap.size()); - - int inputSize = inputMap.size(); - for (int inputIndex = 0; inputIndex < inputSize; inputIndex++) { - String packageName = inputMap.keyAt(inputIndex); - List<ResolveInfo> infos = inputMap.valueAt(inputIndex); + // First, find the package with the latest first install time + String targetPackageName = null; + long latestInstall = Long.MIN_VALUE; + final int size = inputMap.size(); + for (int index = 0; index < size; index++) { + ResolveInfo info = inputMap.keyAt(index); + String packageName = info.getComponentInfo().packageName; PackageSetting pkgSetting = pkgSettingFunction.apply(packageName); + if (pkgSetting == null) { + continue; + } + + long installTime = pkgSetting.getFirstInstallTime(); + if (installTime > latestInstall) { + latestInstall = installTime; + targetPackageName = packageName; + } + } + + // Then, remove all infos that don't match the package + for (int index = inputMap.size() - 1; index >= 0; index--) { + ResolveInfo info = inputMap.keyAt(index); + if (!Objects.equals(targetPackageName, info.getComponentInfo().packageName)) { + inputMap.removeAt(index); + } + } + } + + @NonNull + private void filterToLastDeclared(@NonNull List<ResolveInfo> inputList, + @NonNull Function<String, PackageSetting> pkgSettingFunction) { + // Must call size each time as the size of the list will decrease + for (int index = 0; index < inputList.size(); index++) { + ResolveInfo info = inputList.get(index); + String targetPackageName = info.getComponentInfo().packageName; + PackageSetting pkgSetting = pkgSettingFunction.apply(targetPackageName); AndroidPackage pkg = pkgSetting == null ? null : pkgSetting.getPkg(); if (pkg == null) { continue; } - ResolveInfo result = null; - int highestIndex = -1; - int infosSize = infos.size(); - for (int infoIndex = 0; infoIndex < infosSize; infoIndex++) { - ResolveInfo info = infos.get(infoIndex); - List<ParsedActivity> activities = pkg.getActivities(); - int activitiesSize = activities.size(); - for (int activityIndex = 0; activityIndex < activitiesSize; activityIndex++) { - if (Objects.equals(activities.get(activityIndex).getComponentName(), - info.getComponentInfo().getComponentName())) { - if (activityIndex > highestIndex) { - highestIndex = activityIndex; - result = info; - } - break; - } + ResolveInfo result = info; + int highestIndex = indexOfIntentFilterEntry(pkg, result); + + // Search backwards so that lower results can be removed as they're found + for (int searchIndex = inputList.size() - 1; searchIndex >= index + 1; searchIndex--) { + ResolveInfo searchInfo = inputList.get(searchIndex); + if (!Objects.equals(targetPackageName, searchInfo.getComponentInfo().packageName)) { + continue; } + + int entryIndex = indexOfIntentFilterEntry(pkg, searchInfo); + if (entryIndex > highestIndex) { + highestIndex = entryIndex; + result = searchInfo; + } + + // Always remove the entry so that the current index + // is left as the sole candidate of the target package + inputList.remove(searchIndex); } - // Shouldn't be null, but might as well be safe - if (result != null) { - finalList.add(result); + // Swap the current index for the result, leaving this as + // the only entry with the target package name + inputList.set(index, result); + } + } + + private int indexOfIntentFilterEntry(@NonNull AndroidPackage pkg, + @NonNull ResolveInfo target) { + List<ParsedActivity> activities = pkg.getActivities(); + int activitiesSize = activities.size(); + for (int activityIndex = 0; activityIndex < activitiesSize; activityIndex++) { + if (Objects.equals(activities.get(activityIndex).getComponentName(), + target.getComponentInfo().getComponentName())) { + return activityIndex; } } - return finalList; + return -1; } @Override @@ -1484,8 +1510,7 @@ public class DomainVerificationService extends SystemService @NonNull List<ResolveInfo> candidates, @PackageManager.ResolveInfoFlags int resolveInfoFlags, @UserIdInt int userId) { String packageName = pkgSetting.getName(); - if (!DomainVerificationUtils.isDomainVerificationIntent(intent, candidates, - resolveInfoFlags)) { + if (!DomainVerificationUtils.isDomainVerificationIntent(intent, resolveInfoFlags)) { if (DEBUG_APPROVAL) { debugApproval(packageName, intent, userId, false, "not valid intent"); } diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java index 883bbad1bd2d..cb3b5c9db7e7 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java @@ -22,7 +22,6 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ResolveInfo; import android.os.Binder; import com.android.internal.util.CollectionUtils; @@ -30,7 +29,6 @@ import com.android.server.compat.PlatformCompat; import com.android.server.pm.PackageManagerService; import com.android.server.pm.parsing.pkg.AndroidPackage; -import java.util.List; import java.util.Set; public final class DomainVerificationUtils { @@ -46,7 +44,6 @@ public final class DomainVerificationUtils { } public static boolean isDomainVerificationIntent(Intent intent, - @NonNull List<ResolveInfo> candidates, @PackageManager.ResolveInfoFlags int resolveInfoFlags) { if (!intent.isWebIntent()) { return false; @@ -63,42 +60,18 @@ public final class DomainVerificationUtils { && intent.hasCategory(Intent.CATEGORY_BROWSABLE); } - // In cases where at least one browser is resolved and only one non-browser is resolved, - // the Intent is coerced into an app links intent, under the assumption the browser can - // be skipped if the app is approved at any level for the domain. - boolean foundBrowser = false; - boolean foundOneApp = false; - - final int candidatesSize = candidates.size(); - for (int index = 0; index < candidatesSize; index++) { - final ResolveInfo info = candidates.get(index); - if (info.handleAllWebDataURI) { - foundBrowser = true; - } else if (foundOneApp) { - // Already true, so duplicate app - foundOneApp = false; - break; - } else { - foundOneApp = true; - } - } - boolean matchDefaultByFlags = (resolveInfoFlags & PackageManager.MATCH_DEFAULT_ONLY) != 0; - boolean onlyOneNonBrowser = foundBrowser && foundOneApp; // Check if matches (BROWSABLE || none) && DEFAULT if (categoriesSize == 0) { - // No categories, run coerce case, matching DEFAULT by flags - return onlyOneNonBrowser && matchDefaultByFlags; - } else if (intent.hasCategory(Intent.CATEGORY_DEFAULT)) { - // Run coerce case, matching by explicit DEFAULT - return onlyOneNonBrowser; + // No categories, only allow matching DEFAULT by flags + return matchDefaultByFlags; } else if (intent.hasCategory(Intent.CATEGORY_BROWSABLE)) { // Intent matches BROWSABLE, must match DEFAULT by flags return matchDefaultByFlags; } else { - // Otherwise not matching any app link categories - return false; + // Otherwise only needs to have DEFAULT + return intent.hasCategory(Intent.CATEGORY_DEFAULT); } } |