Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1122021b authored by Winson's avatar Winson
Browse files

Remove app links caller BROWSABLE requirement

Does not require BROWSABLE category on the caller side anymore to apply
app links logic. However, it is now enforced on the receiver side, such
that all IntentFilter matches must contain BROWSABLE to be considered
approved for a domain.

Bug: 181890795

Change-Id: I8d5a10c3b8cc2370a4a69d4abe9ee74b1964f19e
parent 27f0a7d1
Loading
Loading
Loading
Loading
+33 −0
Original line number Diff line number Diff line
@@ -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;
    }

+1 −1
Original line number Diff line number Diff line
@@ -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;
+1 −2
Original line number Diff line number Diff line
@@ -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
+115 −90
Original line number Diff line number Diff line
@@ -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 (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;
            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;
                }
                        break;

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

            // Swap the current index for the result, leaving this as
            // the only entry with the target package name
            inputList.set(index, result);
        }
    }

            // Shouldn't be null, but might as well be safe
            if (result != null) {
                finalList.add(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");
            }
+4 −31
Original line number Diff line number Diff line
@@ -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);
        }
    }