diff options
7 files changed, 363 insertions, 20 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 00747885a143..5a92fc4e717c 100755 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7033,6 +7033,14 @@ public final class Settings { "webview_data_reduction_proxy_key"; /** + * Whether or not the WebView fallback mechanism should be enabled. + * 0=disabled, 1=enabled. + * @hide + */ + public static final String WEBVIEW_FALLBACK_LOGIC_ENABLED = + "webview_fallback_logic_enabled"; + + /** * Name of the package used as WebView provider (if unset the provider is instead determined * by the system). * @hide diff --git a/core/java/android/webkit/IWebViewUpdateService.aidl b/core/java/android/webkit/IWebViewUpdateService.aidl index 89d5d69de392..5697dfc0188c 100644 --- a/core/java/android/webkit/IWebViewUpdateService.aidl +++ b/core/java/android/webkit/IWebViewUpdateService.aidl @@ -38,10 +38,14 @@ interface IWebViewUpdateService { WebViewProviderResponse waitForAndGetProvider(); /** - * DevelopmentSettings uses this to notify WebViewUpdateService that a - * new provider has been selected by the user. + * DevelopmentSettings uses this to notify WebViewUpdateService that a new provider has been + * selected by the user. Returns the provider we end up switching to, this could be different to + * the one passed as argument to this method since the Dev Setting calling this method could be + * stale. I.e. the Dev setting could be letting the user choose uninstalled/disabled packages, + * it would then try to update the provider to such a package while in reality the update + * service would switch to another one. */ - void changeProviderAndSetting(String newProvider); + String changeProviderAndSetting(String newProvider); /** * DevelopmentSettings uses this to get the current available WebView @@ -53,4 +57,15 @@ interface IWebViewUpdateService { * Used by DevelopmentSetting to get the name of the WebView provider currently in use. */ String getCurrentWebViewPackageName(); + + /** + * Used by Settings to determine whether a certain package can be enabled/disabled by the user - + * the package should not be modifiable in this way if it is a fallback package. + */ + boolean isFallbackPackage(String packageName); + + /** + * Enable or disable the WebView package fallback mechanism. + */ + void enableFallbackLogic(boolean enable); } diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index b04b4c0e3b11..ad50ff60780e 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -21,6 +21,7 @@ import android.app.ActivityManagerInternal; import android.app.AppGlobals; import android.app.Application; import android.content.Context; +import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -134,6 +135,7 @@ public final class WebViewFactory { // Whether or not the provider must be explicitly chosen by the user to be used. private static String TAG_AVAILABILITY = "availableByDefault"; private static String TAG_SIGNATURE = "signature"; + private static String TAG_FALLBACK = "isFallback"; /** * Reads all signatures at the current depth (within the current provider) from the XML parser. @@ -159,6 +161,7 @@ public final class WebViewFactory { * @hide * */ public static WebViewProviderInfo[] getWebViewPackages() { + int numFallbackPackages = 0; XmlResourceParser parser = null; List<WebViewProviderInfo> webViewProviders = new ArrayList<WebViewProviderInfo>(); try { @@ -182,13 +185,21 @@ public final class WebViewFactory { throw new MissingWebViewPackageException( "WebView provider in framework resources missing description"); } - String availableByDefault = parser.getAttributeValue(null, TAG_AVAILABILITY); - if (availableByDefault == null) { - availableByDefault = "false"; - } - webViewProviders.add( + boolean availableByDefault = "true".equals( + parser.getAttributeValue(null, TAG_AVAILABILITY)); + boolean isFallback = "true".equals( + parser.getAttributeValue(null, TAG_FALLBACK)); + WebViewProviderInfo currentProvider = new WebViewProviderInfo(packageName, description, availableByDefault, - readSignatures(parser))); + isFallback, readSignatures(parser)); + if (currentProvider.isFallbackPackage()) { + numFallbackPackages++; + if (numFallbackPackages > 1) { + throw new AndroidRuntimeException( + "There can be at most one webview fallback package."); + } + } + webViewProviders.add(currentProvider); } else { Log.e(LOGTAG, "Found an element that is not a webview provider"); @@ -641,6 +652,18 @@ public final class WebViewFactory { return result; } + /** + * Returns whether the entire package from an ACTION_PACKAGE_CHANGED intent was changed (rather + * than just one of its components). + * @hide + */ + public static boolean entirePackageChanged(Intent intent) { + String[] componentList = + intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST); + return Arrays.asList(componentList).contains( + intent.getDataString().substring("package:".length())); + } + private static IWebViewUpdateService getUpdateService() { return IWebViewUpdateService.Stub.asInterface(ServiceManager.getService("webviewupdate")); } diff --git a/core/java/android/webkit/WebViewProviderInfo.java b/core/java/android/webkit/WebViewProviderInfo.java index 94e8b70ce38c..64c2caa58fd5 100644 --- a/core/java/android/webkit/WebViewProviderInfo.java +++ b/core/java/android/webkit/WebViewProviderInfo.java @@ -40,11 +40,12 @@ public class WebViewProviderInfo implements Parcelable { public WebViewPackageNotFoundException(Exception e) { super(e); } } - public WebViewProviderInfo(String packageName, String description, String availableByDefault, - String[] signatures) { + public WebViewProviderInfo(String packageName, String description, boolean availableByDefault, + boolean isFallback, String[] signatures) { this.packageName = packageName; this.description = description; - this.availableByDefault = availableByDefault.equals("true"); + this.availableByDefault = availableByDefault; + this.isFallback = isFallback; this.signatures = signatures; } @@ -114,6 +115,10 @@ public class WebViewProviderInfo implements Parcelable { return availableByDefault; } + public boolean isFallbackPackage() { + return isFallback; + } + private void updatePackageInfo() { try { PackageManager pm = AppGlobals.getInitialApplication().getPackageManager(); @@ -165,6 +170,7 @@ public class WebViewProviderInfo implements Parcelable { public String packageName; public String description; private boolean availableByDefault; + private boolean isFallback; private String[] signatures; diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 3c03a4ada544..b36b279971a1 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -682,8 +682,8 @@ <string name="select_webview_provider_title">WebView implementation</string> <!-- Developer settings: select WebView provider dialog title [CHAR LIMIT=30] --> <string name="select_webview_provider_dialog_title">Set WebView implementation</string> - <!-- Developer settings: confirmation dialog text for the WebView provider selection dialog [CHAR LIMIT=NONE] --> - <string name="select_webview_provider_confirmation_text">The chosen WebView implementation is disabled, and must be enabled to be used, do you wish to enable it?</string> + <!-- Developer settings: text for the WebView provider selection toast shown if an invalid provider was chosen (i.e. the setting list was stale). [CHAR LIMIT=NONE] --> + <string name="select_webview_provider_toast_text">The chosen WebView implementation is invalid because the list of implementation choices grew stale. The list should now be updated.</string> <!-- Developer settings screen, convert userdata to file encryption option name --> <string name="convert_to_file_encryption">Convert to file encryption</string> diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java index f3b120f944fd..1a8f5060f5f1 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java @@ -23,13 +23,18 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageDeleteObserver; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; +import android.content.pm.UserInfo; import android.os.Binder; +import android.os.PatternMatcher; import android.os.Process; import android.os.RemoteException; +import android.os.ResultReceiver; import android.os.UserHandle; +import android.os.UserManager; import android.provider.Settings; import android.provider.Settings.Global; import android.util.AndroidRuntimeException; @@ -41,6 +46,7 @@ import android.webkit.WebViewFactory; import com.android.server.SystemService; +import java.io.FileDescriptor; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -102,6 +108,27 @@ public class WebViewUpdateService extends SystemService { return; } + // Ensure that we only heed PACKAGE_CHANGED intents if they change an entire + // package, not just a component + if (intent.getAction().equals(Intent.ACTION_PACKAGE_CHANGED)) { + if (!WebViewFactory.entirePackageChanged(intent)) { + return; + } + } + + if (intent.getAction().equals(Intent.ACTION_USER_ADDED)) { + int userId = + intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); + handleNewUser(userId); + return; + } + + updateFallbackState(context, intent); + + // TODO(gsennton) for now don't update WebView on PACKAGE_CHANGED as this will + // change the current behaviour even more, instead do this in a follow-up. + if (intent.getAction().equals(Intent.ACTION_PACKAGE_CHANGED)) return; + for (WebViewProviderInfo provider : WebViewFactory.getWebViewPackages()) { String webviewPackage = "package:" + provider.packageName; @@ -154,12 +181,167 @@ public class WebViewUpdateService extends SystemService { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_PACKAGE_ADDED); filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); filter.addDataScheme("package"); + // Make sure we only receive intents for WebView packages from our config file. + for (WebViewProviderInfo provider : WebViewFactory.getWebViewPackages()) { + filter.addDataSchemeSpecificPart(provider.packageName, PatternMatcher.PATTERN_LITERAL); + } getContext().registerReceiver(mWebViewUpdatedReceiver, filter); + IntentFilter userAddedFilter = new IntentFilter(); + userAddedFilter.addAction(Intent.ACTION_USER_ADDED); + getContext().registerReceiver(mWebViewUpdatedReceiver, userAddedFilter); + publishBinderService("webviewupdate", new BinderService()); } + private static boolean existsValidNonFallbackProvider(WebViewProviderInfo[] providers) { + for (WebViewProviderInfo provider : providers) { + if (provider.isAvailableByDefault() && provider.isEnabled() + && provider.isValidProvider() && !provider.isFallbackPackage()) { + return true; + } + } + return false; + } + + private static void enablePackageForUser(String packageName, boolean enable, int userId) { + try { + AppGlobals.getPackageManager().setApplicationEnabledSetting( + packageName, + enable ? PackageManager.COMPONENT_ENABLED_STATE_DEFAULT : + PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0, + userId, null); + } catch (RemoteException e) { + Slog.w(TAG, "Tried to disable " + packageName + " for user " + userId + ": " + e); + } + } + + /** + * Called when a new user has been added to update the state of its fallback package. + */ + void handleNewUser(int userId) { + if (!isFallbackLogicEnabled()) return; + + WebViewProviderInfo[] webviewProviders = WebViewFactory.getWebViewPackages(); + WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders); + if (fallbackProvider == null) return; + boolean existsValidNonFallbackProvider = + existsValidNonFallbackProvider(webviewProviders); + + enablePackageForUser(fallbackProvider.packageName, !existsValidNonFallbackProvider, + userId); + } + + /** + * Handle the enabled-state of our fallback package, i.e. if there exists some non-fallback + * package that is valid (and available by default) then disable the fallback package, + * otherwise, enable the fallback package. + */ + void updateFallbackState(final Context context, final Intent intent) { + if (!isFallbackLogicEnabled()) return; + + WebViewProviderInfo[] webviewProviders = WebViewFactory.getWebViewPackages(); + + if (intent != null && (intent.getAction().equals(Intent.ACTION_PACKAGE_ADDED) + || intent.getAction().equals(Intent.ACTION_PACKAGE_CHANGED))) { + // A package was changed / updated / downgraded, early out if it is not one of the + // webview packages that are available by default. + String changedPackage = null; + for (WebViewProviderInfo provider : webviewProviders) { + String webviewPackage = "package:" + provider.packageName; + if (webviewPackage.equals(intent.getDataString())) { + if (provider.isAvailableByDefault()) { + changedPackage = provider.packageName; + } + break; + } + } + if (changedPackage == null) return; + } + + // 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); + + 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. + && (fallbackProvider.isEnabled() || intent == null)) { + // Uninstall and disable fallback package for all users. + context.getPackageManager().deletePackage(fallbackProvider.packageName, + new IPackageDeleteObserver.Stub() { + public void packageDeleted(String packageName, int returnCode) { + // Ignore returnCode since the deletion could fail, e.g. we might be trying + // to delete a non-updated system-package (and we should still disable the + // package) + UserManager userManager = + (UserManager)context.getSystemService(Context.USER_SERVICE); + // Disable the fallback package for all users. + for(UserInfo userInfo : userManager.getUsers()) { + enablePackageForUser(packageName, false, userInfo.id); + } + } + }, PackageManager.DELETE_SYSTEM_APP | PackageManager.DELETE_ALL_USERS); + } 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. + && (!fallbackProvider.isEnabled() || intent==null)) { + // Enable the fallback package for all users. + UserManager userManager = + (UserManager)context.getSystemService(Context.USER_SERVICE); + for(UserInfo userInfo : userManager.getUsers()) { + enablePackageForUser(fallbackProvider.packageName, true, userInfo.id); + } + } + } + + private static boolean isFallbackLogicEnabled() { + // Note that this is enabled by default (i.e. if the setting hasn't been set). + return Settings.Global.getInt(AppGlobals.getInitialApplication().getContentResolver(), + Settings.Global.WEBVIEW_FALLBACK_LOGIC_ENABLED, 1) == 1; + } + + private static void enableFallbackLogic(boolean enable) { + Settings.Global.putInt(AppGlobals.getInitialApplication().getContentResolver(), + Settings.Global.WEBVIEW_FALLBACK_LOGIC_ENABLED, enable ? 1 : 0); + } + + /** + * Returns the only fallback provider, or null if there is none. + */ + private static WebViewProviderInfo getFallbackProvider(WebViewProviderInfo[] webviewPackages) { + for (WebViewProviderInfo provider : webviewPackages) { + if (provider.isFallbackPackage()) { + return provider; + } + } + return null; + } + + private static boolean containsAvailableNonFallbackProvider( + WebViewProviderInfo[] webviewPackages) { + for (WebViewProviderInfo provider : webviewPackages) { + if (provider.isAvailableByDefault() && provider.isEnabled() + && provider.isValidProvider() && !provider.isFallbackPackage()) { + return true; + } + } + return false; + } + + private static boolean isFallbackPackage(String packageName) { + if (packageName == null || !isFallbackLogicEnabled()) return false; + + WebViewProviderInfo[] webviewPackages = WebViewFactory.getWebViewPackages(); + WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewPackages); + return (fallbackProvider != null + && packageName.equals(fallbackProvider.packageName)); + } + /** * Perform any WebView loading preparations that must happen at boot from the system server, * after the package manager has started or after an update to the webview is installed. @@ -167,6 +349,7 @@ public class WebViewUpdateService extends SystemService { * Currently, this means spawning the child processes which will create the relro files. */ public void prepareWebViewInSystemServer() { + updateFallbackState(getContext(), null); try { synchronized(this) { updateValidWebViewPackages(); @@ -182,8 +365,10 @@ public class WebViewUpdateService extends SystemService { /** * Change WebView provider and provider setting and kill packages using the old provider. + * Return the new provider (in case we are in the middle of creating relro files this new + * provider will not be in use directly, but will when the relros are done). */ - private void changeProviderAndSetting(String newProviderName) { + private String changeProviderAndSetting(String newProviderName) { PackageInfo oldPackage = null; PackageInfo newPackage = null; synchronized(this) { @@ -195,14 +380,14 @@ public class WebViewUpdateService extends SystemService { if (oldPackage != null && newPackage.packageName.equals(oldPackage.packageName)) { // If we don't perform the user change, revert the settings change. updateUserSetting(newPackage.packageName); - return; + return newPackage.packageName; } } catch (WebViewFactory.MissingWebViewPackageException e) { 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; + return newProviderName; } onWebViewProviderChanged(newPackage); } @@ -214,7 +399,7 @@ public class WebViewUpdateService extends SystemService { } } catch (RemoteException e) { } - return; + return newPackage.packageName; } /** @@ -349,6 +534,14 @@ public class WebViewUpdateService extends SystemService { private class BinderService extends IWebViewUpdateService.Stub { + @Override + public void onShellCommand(FileDescriptor in, FileDescriptor out, + FileDescriptor err, String[] args, ResultReceiver resultReceiver) { + (new WebViewUpdateServiceShellCommand(this)).exec( + this, in, out, err, args, resultReceiver); + } + + /** * The shared relro process calls this to notify us that it's done trying to create a relro * file. This method gets called even if the relro creation has failed or the process @@ -423,7 +616,7 @@ public class WebViewUpdateService extends SystemService { * This is called from DeveloperSettings when the user changes WebView provider. */ @Override // Binder call - public void changeProviderAndSetting(String newProvider) { + public String changeProviderAndSetting(String newProvider) { if (getContext().checkCallingPermission( android.Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) { @@ -435,7 +628,7 @@ public class WebViewUpdateService extends SystemService { throw new SecurityException(msg); } - WebViewUpdateService.this.changeProviderAndSetting(newProvider); + return WebViewUpdateService.this.changeProviderAndSetting(newProvider); } @Override // Binder call @@ -453,5 +646,26 @@ public class WebViewUpdateService extends SystemService { return WebViewUpdateService.this.mCurrentWebViewPackage.packageName; } } + + @Override // Binder call + public boolean isFallbackPackage(String packageName) { + return WebViewUpdateService.isFallbackPackage(packageName); + } + + @Override // Binder call + public void enableFallbackLogic(boolean enable) { + if (getContext().checkCallingPermission( + android.Manifest.permission.WRITE_SECURE_SETTINGS) + != PackageManager.PERMISSION_GRANTED) { + String msg = "Permission Denial: enableFallbackLogic() from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid() + + " requires " + android.Manifest.permission.WRITE_SECURE_SETTINGS; + Slog.w(TAG, msg); + throw new SecurityException(msg); + } + + WebViewUpdateService.enableFallbackLogic(enable); + } } } diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand.java new file mode 100644 index 000000000000..a9461e82a596 --- /dev/null +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016 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.webkit; + +import android.os.RemoteException; +import android.os.ShellCommand; +import android.webkit.IWebViewUpdateService; + +import java.io.PrintWriter; + +class WebViewUpdateServiceShellCommand extends ShellCommand { + final IWebViewUpdateService mInterface; + + WebViewUpdateServiceShellCommand(IWebViewUpdateService service) { + mInterface = service; + } + + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(cmd); + } + + final PrintWriter pw = getOutPrintWriter(); + try { + // TODO(gsennton) add command for changing WebView provider + switch(cmd) { + case "enable-redundant-packages": + return enableFallbackLogic(false); + case "disable-redundant-packages": + return enableFallbackLogic(true); + default: + return handleDefaultCommands(cmd); + } + } catch (RemoteException e) { + pw.println("Remote exception: " + e); + } + return -1; + } + + private int enableFallbackLogic(boolean enable) throws RemoteException { + final PrintWriter pw = getOutPrintWriter(); + mInterface.enableFallbackLogic(enable); + pw.println("Success"); + return 0; + } + + @Override + public void onHelp() { + PrintWriter pw = getOutPrintWriter(); + pw.println("WebView updater commands:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(""); + pw.println(" enable-redundant-packages"); + pw.println(" Allow a fallback package to be installed and enabled even when a"); + pw.println(" more-preferred package is available. This command is useful when testing"); + pw.println(" fallback packages."); + pw.println(" disable-redundant-packages"); + pw.println(" Disallow installing and enabling fallback packages when a more-preferred"); + pw.println(" package is available."); + pw.println(); + } +} |