Add utility interface for WebView preparation logic.
To make the WebView preparation mechanism testable we add a utility
interface that can be overridden during a test to avoid calling the
Android framework and to provide custom WebView packages.
With this change we also split some of the code from the WebViewFactory
(code unrelated to WebView loading) into a separate utility class.
Bug: 27635535
Change-Id: I265ecd42b24ad5383637e125b3654ff339c9df9c
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
index 2db6b5d..181b807 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
@@ -35,14 +35,14 @@
import android.os.ResultReceiver;
import android.os.UserHandle;
import android.os.UserManager;
-import android.provider.Settings;
import android.provider.Settings.Global;
+import android.provider.Settings;
import android.util.AndroidRuntimeException;
import android.util.Slog;
import android.webkit.IWebViewUpdateService;
+import android.webkit.WebViewFactory;
import android.webkit.WebViewProviderInfo;
import android.webkit.WebViewProviderResponse;
-import android.webkit.WebViewFactory;
import com.android.server.SystemService;
@@ -78,9 +78,11 @@
private WebViewProviderInfo[] mCurrentValidWebViewPackages = null;
private BroadcastReceiver mWebViewUpdatedReceiver;
+ private WebViewUtilityInterface mWebViewUtility;
public WebViewUpdateService(Context context) {
super(context);
+ mWebViewUtility = new WebViewUtilityImpl();
}
@Override
@@ -125,7 +127,7 @@
updateFallbackState(context, intent);
- for (WebViewProviderInfo provider : WebViewFactory.getWebViewPackages()) {
+ for (WebViewProviderInfo provider : mWebViewUtility.getWebViewPackages()) {
String webviewPackage = "package:" + provider.packageName;
if (webviewPackage.equals(intent.getDataString())) {
@@ -164,11 +166,7 @@
// package that was not the previous provider then we must kill
// packages dependent on the old package ourselves. The framework
// only kills dependents of packages that are being removed.
- try {
- ActivityManagerNative.getDefault().killPackageDependents(
- oldProviderName, UserHandle.USER_ALL);
- } catch (RemoteException e) {
- }
+ mWebViewUtility.killPackageDependents(oldProviderName);
}
return;
}
@@ -181,7 +179,7 @@
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()) {
+ for (WebViewProviderInfo provider : mWebViewUtility.getWebViewPackages()) {
filter.addDataSchemeSpecificPart(provider.packageName, PatternMatcher.PATTERN_LITERAL);
}
getContext().registerReceiver(mWebViewUpdatedReceiver, filter);
@@ -221,7 +219,7 @@
void handleNewUser(int userId) {
if (!isFallbackLogicEnabled()) return;
- WebViewProviderInfo[] webviewProviders = WebViewFactory.getWebViewPackages();
+ WebViewProviderInfo[] webviewProviders = mWebViewUtility.getWebViewPackages();
WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders);
if (fallbackProvider == null) return;
boolean existsValidNonFallbackProvider =
@@ -239,7 +237,7 @@
void updateFallbackState(final Context context, final Intent intent) {
if (!isFallbackLogicEnabled()) return;
- WebViewProviderInfo[] webviewProviders = WebViewFactory.getWebViewPackages();
+ WebViewProviderInfo[] webviewProviders = mWebViewUtility.getWebViewPackages();
if (intent != null && (intent.getAction().equals(Intent.ACTION_PACKAGE_ADDED)
|| intent.getAction().equals(Intent.ACTION_PACKAGE_CHANGED))) {
@@ -330,10 +328,10 @@
return false;
}
- private static boolean isFallbackPackage(String packageName) {
+ private boolean isFallbackPackage(String packageName) {
if (packageName == null || !isFallbackLogicEnabled()) return false;
- WebViewProviderInfo[] webviewPackages = WebViewFactory.getWebViewPackages();
+ WebViewProviderInfo[] webviewPackages = mWebViewUtility.getWebViewPackages();
WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewPackages);
return (fallbackProvider != null
&& packageName.equals(fallbackProvider.packageName));
@@ -370,13 +368,13 @@
PackageInfo newPackage = null;
synchronized(this) {
oldPackage = mCurrentWebViewPackage;
- updateUserSetting(newProviderName);
+ mWebViewUtility.updateUserSetting(getContext(), newProviderName);
try {
newPackage = findPreferredWebViewPackage();
if (oldPackage != null && newPackage.packageName.equals(oldPackage.packageName)) {
// If we don't perform the user change, revert the settings change.
- updateUserSetting(newPackage.packageName);
+ mWebViewUtility.updateUserSetting(getContext(), newPackage.packageName);
return newPackage.packageName;
}
} catch (WebViewFactory.MissingWebViewPackageException e) {
@@ -389,12 +387,8 @@
onWebViewProviderChanged(newPackage);
}
// Kill apps using the old provider
- try {
- if (oldPackage != null) {
- ActivityManagerNative.getDefault().killPackageDependents(
- oldPackage.packageName, UserHandle.USER_ALL);
- }
- } catch (RemoteException e) {
+ if (oldPackage != null) {
+ mWebViewUtility.killPackageDependents(oldPackage.packageName);
}
return newPackage.packageName;
}
@@ -411,14 +405,14 @@
mCurrentProviderBeingReplaced = false;
if (mNumRelroCreationsStarted == mNumRelroCreationsFinished) {
mCurrentWebViewPackage = newPackage;
- updateUserSetting(newPackage.packageName);
+ mWebViewUtility.updateUserSetting(getContext(), newPackage.packageName);
// The relro creations might 'finish' (not start at all) before
// WebViewFactory.onWebViewProviderChanged which means we might not know the number
// of started creations before they finish.
mNumRelroCreationsStarted = NUMBER_OF_RELROS_UNKNOWN;
mNumRelroCreationsFinished = 0;
- mNumRelroCreationsStarted = WebViewFactory.onWebViewProviderChanged(newPackage);
+ mNumRelroCreationsStarted = mWebViewUtility.onWebViewProviderChanged(newPackage);
// If the relro creations finish before we know the number of started creations we
// will have to do any cleanup/notifying here.
checkIfRelrosDoneLocked();
@@ -435,7 +429,7 @@
* */
private void updateValidWebViewPackages() {
List<WebViewProviderInfo> webViewProviders =
- new ArrayList<WebViewProviderInfo>(Arrays.asList(WebViewFactory.getWebViewPackages()));
+ new ArrayList<WebViewProviderInfo>(Arrays.asList(mWebViewUtility.getWebViewPackages()));
Iterator<WebViewProviderInfo> it = webViewProviders.iterator();
// remove non-valid packages
while(it.hasNext()) {
@@ -449,17 +443,6 @@
}
}
- private static String getUserChosenWebViewProvider() {
- return Settings.Global.getString(AppGlobals.getInitialApplication().getContentResolver(),
- Settings.Global.WEBVIEW_PROVIDER);
- }
-
- private void updateUserSetting(String newProviderName) {
- Settings.Global.putString(getContext().getContentResolver(),
- Settings.Global.WEBVIEW_PROVIDER,
- newProviderName == null ? "" : newProviderName);
- }
-
/**
* Returns either the package info of the WebView provider determined in the following way:
* If the user has chosen a provider then use that if it is valid,
@@ -470,7 +453,7 @@
private PackageInfo findPreferredWebViewPackage() {
WebViewProviderInfo[] providers = mCurrentValidWebViewPackages;
- String userChosenProvider = getUserChosenWebViewProvider();
+ String userChosenProvider = mWebViewUtility.getUserChosenWebViewProvider(getContext());
// If the user has chosen provider, use that
for (WebViewProviderInfo provider : providers) {
@@ -641,6 +624,11 @@
}
@Override // Binder call
+ public WebViewProviderInfo[] getAllWebViewPackages() {
+ return WebViewUpdateService.this.mWebViewUtility.getWebViewPackages();
+ }
+
+ @Override // Binder call
public String getCurrentWebViewPackageName() {
synchronized(WebViewUpdateService.this) {
if (WebViewUpdateService.this.mCurrentWebViewPackage == null)
@@ -651,7 +639,7 @@
@Override // Binder call
public boolean isFallbackPackage(String packageName) {
- return WebViewUpdateService.isFallbackPackage(packageName);
+ return WebViewUpdateService.this.isFallbackPackage(packageName);
}
@Override // Binder call
diff --git a/services/core/java/com/android/server/webkit/WebViewUtilityImpl.java b/services/core/java/com/android/server/webkit/WebViewUtilityImpl.java
new file mode 100644
index 0000000..4dbd02d
--- /dev/null
+++ b/services/core/java/com/android/server/webkit/WebViewUtilityImpl.java
@@ -0,0 +1,161 @@
+/*
+ * 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.app.ActivityManagerNative;
+import android.app.AppGlobals;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.res.XmlResourceParser;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.AndroidRuntimeException;
+import android.util.Log;
+import android.webkit.WebViewFactory;
+import android.webkit.WebViewFactory.MissingWebViewPackageException;
+import android.webkit.WebViewProviderInfo;
+
+import com.android.internal.util.XmlUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * Default implementation for the WebView preparation Utility interface.
+ * @hide
+ */
+public class WebViewUtilityImpl implements WebViewUtilityInterface {
+ private static final String TAG = WebViewUtilityImpl.class.getSimpleName();
+ private static final String TAG_START = "webviewproviders";
+ private static final String TAG_WEBVIEW_PROVIDER = "webviewprovider";
+ private static final String TAG_PACKAGE_NAME = "packageName";
+ private static final String TAG_DESCRIPTION = "description";
+ // Whether or not the provider must be explicitly chosen by the user to be used.
+ private static final String TAG_AVAILABILITY = "availableByDefault";
+ private static final String TAG_SIGNATURE = "signature";
+ private static final String TAG_FALLBACK = "isFallback";
+
+ /**
+ * Returns all packages declared in the framework resources as potential WebView providers.
+ * @hide
+ * */
+ @Override
+ public WebViewProviderInfo[] getWebViewPackages() {
+ int numFallbackPackages = 0;
+ XmlResourceParser parser = null;
+ List<WebViewProviderInfo> webViewProviders = new ArrayList<WebViewProviderInfo>();
+ try {
+ parser = AppGlobals.getInitialApplication().getResources().getXml(
+ com.android.internal.R.xml.config_webview_packages);
+ XmlUtils.beginDocument(parser, TAG_START);
+ while(true) {
+ XmlUtils.nextElement(parser);
+ String element = parser.getName();
+ if (element == null) {
+ break;
+ }
+ if (element.equals(TAG_WEBVIEW_PROVIDER)) {
+ String packageName = parser.getAttributeValue(null, TAG_PACKAGE_NAME);
+ if (packageName == null) {
+ throw new MissingWebViewPackageException(
+ "WebView provider in framework resources missing package name");
+ }
+ String description = parser.getAttributeValue(null, TAG_DESCRIPTION);
+ if (description == null) {
+ throw new MissingWebViewPackageException(
+ "WebView provider in framework resources missing description");
+ }
+ 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,
+ 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(TAG, "Found an element that is not a webview provider");
+ }
+ }
+ } catch(XmlPullParserException e) {
+ throw new MissingWebViewPackageException("Error when parsing WebView meta data " + e);
+ } catch(IOException e) {
+ throw new MissingWebViewPackageException("Error when parsing WebView meta data " + e);
+ } finally {
+ if (parser != null) parser.close();
+ }
+ return webViewProviders.toArray(new WebViewProviderInfo[webViewProviders.size()]);
+ }
+
+ /**
+ * Reads all signatures at the current depth (within the current provider) from the XML parser.
+ */
+ private static String[] readSignatures(XmlResourceParser parser) throws IOException,
+ XmlPullParserException {
+ List<String> signatures = new ArrayList<String>();
+ int outerDepth = parser.getDepth();
+ while(XmlUtils.nextElementWithin(parser, outerDepth)) {
+ if (parser.getName().equals(TAG_SIGNATURE)) {
+ // Parse the value within the signature tag
+ String signature = parser.nextText();
+ signatures.add(signature);
+ } else {
+ Log.e(TAG, "Found an element in a webview provider that is not a signature");
+ }
+ }
+ return signatures.toArray(new String[signatures.size()]);
+ }
+
+ @Override
+ public int onWebViewProviderChanged(PackageInfo packageInfo) {
+ return WebViewFactory.onWebViewProviderChanged(packageInfo);
+ }
+
+ @Override
+ public String getUserChosenWebViewProvider(Context context) {
+ return Settings.Global.getString(context.getContentResolver(),
+ Settings.Global.WEBVIEW_PROVIDER);
+ }
+
+ @Override
+ public void updateUserSetting(Context context, String newProviderName) {
+ Settings.Global.putString(context.getContentResolver(),
+ Settings.Global.WEBVIEW_PROVIDER,
+ newProviderName == null ? "" : newProviderName);
+ }
+
+ @Override
+ public void killPackageDependents(String packageName) {
+ try {
+ ActivityManagerNative.getDefault().killPackageDependents(packageName,
+ UserHandle.USER_ALL);
+ } catch (RemoteException e) {
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/webkit/WebViewUtilityInterface.java b/services/core/java/com/android/server/webkit/WebViewUtilityInterface.java
new file mode 100644
index 0000000..1919f40
--- /dev/null
+++ b/services/core/java/com/android/server/webkit/WebViewUtilityInterface.java
@@ -0,0 +1,37 @@
+/*
+ * 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.webkit.WebViewProviderInfo;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+
+/**
+ * Utility interface for the WebViewUpdateService.
+ * This interface provides a way to test the WebView preparation mechanism - during normal use this
+ * interface is implemented using calls to the Android framework, but by providing an alternative
+ * implementation we can test the WebView preparation logic without reaching other framework code.
+ * @hide
+ */
+public interface WebViewUtilityInterface {
+ public WebViewProviderInfo[] getWebViewPackages();
+ public int onWebViewProviderChanged(PackageInfo packageInfo);
+
+ public String getUserChosenWebViewProvider(Context context);
+ public void updateUserSetting(Context context, String newProviderName);
+ public void killPackageDependents(String packageName);
+}