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

Commit 364e1602 authored by Gustav Sennton's avatar Gustav Sennton
Browse files

Change WebView fallback mechanism to consider all users' package states.

MultiProcess WebView will use a Service to start a separate process (the
renderer process). This Service can only be started if the WebView
package is enabled for the current user. To ensure that the current
WebView package is usable by all users on a single device we now only
use a WebView package as WebView implementation if that package is
enabled and installed for all user on the device. This also means that
the WebView-fallback mechanism will trigger when disabling the primary
WebView package for any user (not just the system user).

Also add multi-user unit tests to cover this new change.

Bug: 32894152
Test: run unit tests in WebViewUpdateServiceTest
Test: ensure the standalone WebView package becomes enabled (for all
device users) when disabling Chrome for a secondary user.
Test: load WebView (both using Monochrome, and using the standalone
WebView).

Change-Id: Iad3fc48aa50273062c2f29ae48a343c2dea38116
parent 1628b97e
Loading
Loading
Loading
Loading
+94 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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 android.webkit;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.UserInfo;
import android.os.UserManager;

import java.util.ArrayList;
import java.util.List;

/**
 * Utility class for storing a (user,PackageInfo) mapping.
 * @hide
 */
public class UserPackage {
    private final UserInfo mUserInfo;
    private final PackageInfo mPackageInfo;

    public UserPackage(UserInfo user, PackageInfo packageInfo) {
        this.mUserInfo = user;
        this.mPackageInfo = packageInfo;
    }

    /**
     * Returns a list of (User,PackageInfo) pairs corresponding to the PackageInfos for all
     * device users for the package named {@param packageName}.
     */
    public static List<UserPackage> getPackageInfosAllUsers(Context context,
            String packageName, int packageFlags) {
        List<UserInfo> users = getAllUsers(context);
        List<UserPackage> userPackages = new ArrayList<UserPackage>(users.size());
        for (UserInfo user : users) {
            PackageInfo packageInfo = null;
            try {
                packageInfo = context.getPackageManager().getPackageInfoAsUser(
                        packageName, packageFlags, user.id);
            } catch (NameNotFoundException e) {
            }
            userPackages.add(new UserPackage(user, packageInfo));
        }
        return userPackages;
    }

    /**
     * Returns whether the given package is enabled.
     * This state can be changed by the user from Settings->Apps
     */
    public boolean isEnabledPackage() {
        if (mPackageInfo == null) return false;
        return mPackageInfo.applicationInfo.enabled;
    }

    /**
     * Return true if the package is installed and not hidden
     */
    public boolean isInstalledPackage() {
        if (mPackageInfo == null) return false;
        return (((mPackageInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) != 0)
            && ((mPackageInfo.applicationInfo.privateFlags
                        & ApplicationInfo.PRIVATE_FLAG_HIDDEN) == 0));
    }

    public UserInfo getUserInfo() {
        return mUserInfo;
    }

    public PackageInfo getPackageInfo() {
        return mPackageInfo;
    }


    private static List<UserInfo> getAllUsers(Context context) {
        UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
        return userManager.getUsers(false);
    }

}
+7 −0
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ import android.provider.Settings.Global;
import android.provider.Settings;
import android.util.AndroidRuntimeException;
import android.util.Log;
import android.webkit.UserPackage;
import android.webkit.WebViewFactory;
import android.webkit.WebViewProviderInfo;
import android.webkit.WebViewZygote;
@@ -270,6 +271,12 @@ public class SystemImpl implements SystemInterface {
        return pm.getPackageInfo(configInfo.packageName, PACKAGE_FLAGS);
    }

    @Override
    public List<UserPackage> getPackageInfoForProviderAllUsers(Context context,
            WebViewProviderInfo configInfo) {
        return UserPackage.getPackageInfosAllUsers(context, configInfo.packageName, PACKAGE_FLAGS);
    }

    @Override
    public int getMultiProcessSetting(Context context) {
        return Settings.Global.getInt(context.getContentResolver(),
+11 −0
Original line number Diff line number Diff line
@@ -20,8 +20,11 @@ import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.ContentObserver;
import android.webkit.UserPackage;
import android.webkit.WebViewProviderInfo;

import java.util.List;

/**
 * System interface for the WebViewUpdateService.
 * This interface provides a way to test the WebView preparation mechanism - during normal use this
@@ -49,6 +52,14 @@ public interface SystemInterface {
    public boolean systemIsDebuggable();
    public PackageInfo getPackageInfoForProvider(WebViewProviderInfo configInfo)
            throws NameNotFoundException;
    /**
     * Get the PackageInfos of all users for the package represented by {@param configInfo}.
     * @return an array of UserPackages for a certain package, each UserPackage being belonging to a
     *         certain user. The returned array can contain null PackageInfos if the given package
     *         is uninstalled for some user.
     */
    public List<UserPackage> getPackageInfoForProviderAllUsers(Context context,
            WebViewProviderInfo configInfo);

    public int getMultiProcessSetting(Context context);
    public void setMultiProcessSetting(Context context, int value);
+4 −0
Original line number Diff line number Diff line
@@ -94,6 +94,9 @@ public class WebViewUpdateService extends SystemService {
                        case Intent.ACTION_USER_ADDED:
                            mImpl.handleNewUser(userId);
                            break;
                        case Intent.ACTION_USER_REMOVED:
                            mImpl.handleUserRemoved(userId);
                            break;
                    }
                }
        };
@@ -112,6 +115,7 @@ public class WebViewUpdateService extends SystemService {

        IntentFilter userAddedFilter = new IntentFilter();
        userAddedFilter.addAction(Intent.ACTION_USER_ADDED);
        userAddedFilter.addAction(Intent.ACTION_USER_REMOVED);
        getContext().registerReceiverAsUser(mWebViewUpdatedReceiver, UserHandle.ALL,
                userAddedFilter, null /* broadcast permission */, null /* handler */);

+123 −98
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.os.Handler;
import android.os.UserHandle;
import android.util.Base64;
import android.util.Slog;
import android.webkit.UserPackage;
import android.webkit.WebViewFactory;
import android.webkit.WebViewProviderInfo;
import android.webkit.WebViewProviderResponse;
@@ -100,32 +101,41 @@ public class WebViewUpdateServiceImpl {
    private boolean existsValidNonFallbackProvider(WebViewProviderInfo[] providers) {
        for (WebViewProviderInfo provider : providers) {
            if (provider.availableByDefault && !provider.isFallback) {
                try {
                    PackageInfo packageInfo = mSystemInterface.getPackageInfoForProvider(provider);
                    if (isInstalledPackage(packageInfo) && isEnabledPackage(packageInfo)
                            && mWebViewUpdater.isValidProvider(provider, packageInfo)) {
                // userPackages can contain null objects.
                List<UserPackage> userPackages =
                        mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider);
                if (isInstalledAndEnabledForAllUsers(userPackages) &&
                        // Checking validity of the package for the system user (rather than all
                        // users) since package validity depends not on the user but on the package
                        // itself.
                        mWebViewUpdater.isValidProvider(provider,
                                userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo())) {
                    return true;
                }
                } catch (NameNotFoundException e) {
                    // A non-existent provider is neither valid nor enabled
                }
            }
        }
        return false;
    }

    /**
     * Called when a new user has been added to update the state of its fallback package.
     */
    void handleNewUser(int userId) {
        if (!mSystemInterface.isFallbackLogicEnabled()) return;
        handleUserChange();
    }

        WebViewProviderInfo[] webviewProviders = mSystemInterface.getWebViewPackages();
        WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders);
        if (fallbackProvider == null) return;
    void handleUserRemoved(int userId) {
        handleUserChange();
    }

        mSystemInterface.enablePackageForUser(fallbackProvider.packageName,
                !existsValidNonFallbackProvider(webviewProviders), userId);
    /**
     * Called when a user was added or removed to ensure fallback logic and WebView preparation are
     * triggered. This has to be done since the WebView package we use depends on the enabled-state
     * of packages for all users (so adding or removing a user might cause us to change package).
     */
    private void handleUserChange() {
        if (mSystemInterface.isFallbackLogicEnabled()) {
            updateFallbackState(mSystemInterface.getWebViewPackages());
        }
        // Potentially trigger package-changing logic.
        mWebViewUpdater.updateCurrentWebViewPackage(null);
    }

    void notifyRelroCreationCompleted() {
@@ -141,7 +151,7 @@ public class WebViewUpdateServiceImpl {
    }

    WebViewProviderInfo[] getValidWebViewPackages() {
        return mWebViewUpdater.getValidAndInstalledWebViewPackages();
        return mWebViewUpdater.getValidWebViewPackages();
    }

    WebViewProviderInfo[] getWebViewPackages() {
@@ -160,7 +170,7 @@ public class WebViewUpdateServiceImpl {
        if (!mSystemInterface.isFallbackLogicEnabled()) return;

        WebViewProviderInfo[] webviewProviders = mSystemInterface.getWebViewPackages();
        updateFallbackState(webviewProviders, true);
        updateFallbackState(webviewProviders);
    }

    /**
@@ -185,35 +195,23 @@ public class WebViewUpdateServiceImpl {
            }
        }
        if (!changedPackageAvailableByDefault) return;
        updateFallbackState(webviewProviders, false);
        updateFallbackState(webviewProviders);
    }

    private void updateFallbackState(WebViewProviderInfo[] webviewProviders, boolean isBoot) {
    private void updateFallbackState(WebViewProviderInfo[] webviewProviders) {
        // If there exists a valid and enabled non-fallback package - disable the fallback
        // package, otherwise, enable it.
        WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders);
        if (fallbackProvider == null) return;
        boolean existsValidNonFallbackProvider = existsValidNonFallbackProvider(webviewProviders);

        boolean isFallbackEnabled = false;
        try {
            isFallbackEnabled = isEnabledPackage(
                    mSystemInterface.getPackageInfoForProvider(fallbackProvider));
        } catch (NameNotFoundException e) {
            // No fallback package installed -> early out.
            return;
        }

        if (existsValidNonFallbackProvider
                // During an OTA the primary user's WebView state might differ from other users', so
                // ignore the state of that user during boot.
                && (isFallbackEnabled || isBoot)) {
        List<UserPackage> userPackages =
                mSystemInterface.getPackageInfoForProviderAllUsers(mContext, fallbackProvider);
        if (existsValidNonFallbackProvider && !isDisabledForAllUsers(userPackages)) {
            mSystemInterface.uninstallAndDisablePackageForAllUsers(mContext,
                    fallbackProvider.packageName);
        } else if (!existsValidNonFallbackProvider
                // During an OTA the primary user's WebView state might differ from other users', so
                // ignore the state of that user during boot.
                && (!isFallbackEnabled || isBoot)) {
                && !isInstalledAndEnabledForAllUsers(userPackages)) {
            // Enable the fallback package for all users.
            mSystemInterface.enablePackageForAllUsers(mContext,
                    fallbackProvider.packageName, true);
@@ -376,38 +374,8 @@ public class WebViewUpdateServiceImpl {
         * or the replacement are done).
         */
        public String changeProviderAndSetting(String newProviderName) {
            PackageInfo oldPackage = null;
            PackageInfo newPackage = null;
            boolean providerChanged = false;
            synchronized(mLock) {
                oldPackage = mCurrentWebViewPackage;
                mSystemInterface.updateUserSetting(mContext, newProviderName);

                try {
                    newPackage = findPreferredWebViewPackage();
                    providerChanged = (oldPackage == null)
                            || !newPackage.packageName.equals(oldPackage.packageName);
                } catch (WebViewPackageMissingException e) {
                    mCurrentWebViewPackage = null;
                    Slog.e(TAG, "Tried to change WebView provider but failed to fetch WebView " +
                            "package " + e);
                    // If we don't perform the user change but don't have an installed WebView
                    // package, we will have changed the setting and it will be used when a package
                    // is available.
                    return "";
                }
                // Perform the provider change if we chose a new provider
                if (providerChanged) {
                    onWebViewProviderChanged(newPackage);
                }
            }
            // Kill apps using the old provider only if we changed provider
            if (providerChanged && oldPackage != null) {
                mSystemInterface.killPackageDependents(oldPackage.packageName);
            }
            // Return the new provider, this is not necessarily the one we were asked to switch to
            // But the persistent setting will now be pointing to the provider we were asked to
            // switch to anyway
            PackageInfo newPackage = updateCurrentWebViewPackage(newProviderName);
            if (newPackage == null) return "";
            return newPackage.packageName;
        }

@@ -437,15 +405,14 @@ public class WebViewUpdateServiceImpl {
            }
        }

        private ProviderAndPackageInfo[] getValidWebViewPackagesAndInfos(boolean onlyInstalled) {
        private ProviderAndPackageInfo[] getValidWebViewPackagesAndInfos() {
            WebViewProviderInfo[] allProviders = mSystemInterface.getWebViewPackages();
            List<ProviderAndPackageInfo> providers = new ArrayList<>();
            for(int n = 0; n < allProviders.length; n++) {
                try {
                    PackageInfo packageInfo =
                        mSystemInterface.getPackageInfoForProvider(allProviders[n]);
                    if ((!onlyInstalled || isInstalledPackage(packageInfo))
                            && isValidProvider(allProviders[n], packageInfo)) {
                    if (isValidProvider(allProviders[n], packageInfo)) {
                        providers.add(new ProviderAndPackageInfo(allProviders[n], packageInfo));
                    }
                } catch (NameNotFoundException e) {
@@ -458,9 +425,8 @@ public class WebViewUpdateServiceImpl {
        /**
         * Fetch only the currently valid WebView packages.
         **/
        public WebViewProviderInfo[] getValidAndInstalledWebViewPackages() {
            ProviderAndPackageInfo[] providersAndPackageInfos =
                getValidWebViewPackagesAndInfos(true /* only fetch installed packages */);
        public WebViewProviderInfo[] getValidWebViewPackages() {
            ProviderAndPackageInfo[] providersAndPackageInfos = getValidWebViewPackagesAndInfos();
            WebViewProviderInfo[] providers =
                new WebViewProviderInfo[providersAndPackageInfos.length];
            for(int n = 0; n < providersAndPackageInfos.length; n++) {
@@ -487,39 +453,49 @@ public class WebViewUpdateServiceImpl {
         *
         */
        private PackageInfo findPreferredWebViewPackage() throws WebViewPackageMissingException {
            ProviderAndPackageInfo[] providers =
                getValidWebViewPackagesAndInfos(false /* onlyInstalled */);
            ProviderAndPackageInfo[] providers = getValidWebViewPackagesAndInfos();

            String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider(mContext);

            // If the user has chosen provider, use that
            // If the user has chosen provider, use that (if it's installed and enabled for all
            // users).
            for (ProviderAndPackageInfo providerAndPackage : providers) {
                if (providerAndPackage.provider.packageName.equals(userChosenProvider)
                        && isInstalledPackage(providerAndPackage.packageInfo)
                        && isEnabledPackage(providerAndPackage.packageInfo)) {
                if (providerAndPackage.provider.packageName.equals(userChosenProvider)) {
                    // userPackages can contain null objects.
                    List<UserPackage> userPackages =
                            mSystemInterface.getPackageInfoForProviderAllUsers(mContext,
                                    providerAndPackage.provider);
                    if (isInstalledAndEnabledForAllUsers(userPackages)) {
                        return providerAndPackage.packageInfo;
                    }
                }
            }

            // User did not choose, or the choice failed; use the most stable provider that is
            // installed and enabled for the device owner, and available by default (not through
            // installed and enabled for all users, and available by default (not through
            // user choice).
            for (ProviderAndPackageInfo providerAndPackage : providers) {
                if (providerAndPackage.provider.availableByDefault
                        && isInstalledPackage(providerAndPackage.packageInfo)
                        && isEnabledPackage(providerAndPackage.packageInfo)) {
                if (providerAndPackage.provider.availableByDefault) {
                    // userPackages can contain null objects.
                    List<UserPackage> userPackages =
                            mSystemInterface.getPackageInfoForProviderAllUsers(mContext,
                                    providerAndPackage.provider);
                    if (isInstalledAndEnabledForAllUsers(userPackages)) {
                        return providerAndPackage.packageInfo;
                    }
                }
            }

            // Could not find any installed and enabled package either, use the most stable and
            // default-available provider.
            // TODO(gsennton) remove this when we have a functional WebView stub.
            for (ProviderAndPackageInfo providerAndPackage : providers) {
                if (providerAndPackage.provider.availableByDefault) {
                    return providerAndPackage.packageInfo;
                }
            }

            // This should never happen during normal operation (only with modified system images).
            mAnyWebViewInstalled = false;
            throw new WebViewPackageMissingException("Could not find a loadable WebView package");
        }
@@ -702,6 +678,48 @@ public class WebViewUpdateServiceImpl {
                        mAnyWebViewInstalled));
            }
        }

        /**
         * Update the current WebView package.
         * @param newProviderName the package to switch to, null if no package has been explicitly
         * chosen.
         */
        public PackageInfo updateCurrentWebViewPackage(String newProviderName) {
            PackageInfo oldPackage = null;
            PackageInfo newPackage = null;
            boolean providerChanged = false;
            synchronized(mLock) {
                oldPackage = mCurrentWebViewPackage;

                if (newProviderName != null) {
                    mSystemInterface.updateUserSetting(mContext, newProviderName);
                }

                try {
                    newPackage = findPreferredWebViewPackage();
                    providerChanged = (oldPackage == null)
                            || !newPackage.packageName.equals(oldPackage.packageName);
                } catch (WebViewPackageMissingException e) {
                    // If updated the Setting but don't have an installed WebView package, the
                    // Setting will be used when a package is available.
                    mCurrentWebViewPackage = null;
                    Slog.e(TAG, "Couldn't find WebView package to use " + e);
                    return null;
                }
                // Perform the provider change if we chose a new provider
                if (providerChanged) {
                    onWebViewProviderChanged(newPackage);
                }
            }
            // Kill apps using the old provider only if we changed provider
            if (providerChanged && oldPackage != null) {
                mSystemInterface.killPackageDependents(oldPackage.packageName);
            }
            // Return the new provider, this is not necessarily the one we were asked to switch to,
            // but the persistent setting will now be pointing to the provider we were asked to
            // switch to anyway.
            return newPackage;
        }
    }

    private static boolean providerHasValidSignature(WebViewProviderInfo provider,
@@ -730,20 +748,27 @@ public class WebViewUpdateServiceImpl {
    }

    /**
     * Returns whether the given package is enabled.
     * This state can be changed by the user from Settings->Apps
     * Return true iff {@param packageInfos} point to only installed and enabled packages.
     * The given packages {@param packageInfos} should all be pointing to the same package, but each
     * PackageInfo representing a different user's package.
     */
    private static boolean isEnabledPackage(PackageInfo packageInfo) {
        return packageInfo.applicationInfo.enabled;
    private static boolean isInstalledAndEnabledForAllUsers(
            List<UserPackage> userPackages) {
        for (UserPackage userPackage : userPackages) {
            if (!userPackage.isInstalledPackage() || !userPackage.isEnabledPackage()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Return true if the package is installed and not hidden
     */
    private static boolean isInstalledPackage(PackageInfo packageInfo) {
        return (((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) != 0)
            && ((packageInfo.applicationInfo.privateFlags
                        & ApplicationInfo.PRIVATE_FLAG_HIDDEN) == 0));
    private static boolean isDisabledForAllUsers(List<UserPackage> userPackages) {
        for (UserPackage userPackage : userPackages) {
            if (userPackage.getPackageInfo() != null && userPackage.isEnabledPackage()) {
                return false;
            }
        }
        return true;
    }

    /**
Loading