Commit 34df84d5 authored by Nate Fischer's avatar Nate Fischer Committed by Commit Bot

AW: fetch flag overrides via ContentProvider

This CL completes the basic implementation of the flags UI. During
startup, the embedded WebView implementation will check if the user has
enabled developer mode, and if so, fetch the flag overrides from the
service.

This uses a ContentProvider instead of an aidl method on the service
interface for the sake of a simple synchronous IPC. Although aidl itself
supports synchronous IPC, the Android framework only supports binding to
the service asynchronously. We require fully synchronous IPC so we can
block startup while we fetch the flags.

Now that we've settled on using a ContentProvider to plumb information
from the developer UI to embedded WebViews, this changes "developer
mode" to be defined by the ContentProvider's state rather than the
Service's state, which has the side benefit of simplifying some of the
Activity/Service code.

We rely on PackageManager APIs to check if developer mode is enabled.
The check itself should have very little impact to startup time, since
PackageManager caches its state in RAM. I benchmarked this check (when
developer mode is disabled) at 0ms on my Google Pixel 2 device. For
simplicity, we do not care about performance when developer mode is
enabled, as this is not the usual user experience.

Bug: 981143
Test: Manual - toggle debug border flag, start WebView shell, see borders
Test: Benchmark isDeveloperModeEnabled() with System.currentTimeMillis()
Test: run_android_webview_junit_tests -f *ServiceNamesTest*
Change-Id: I7cc67d1bdf8f0f2ce0fce714fb359160899354a7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1977828
Commit-Queue: Nate Fischer <ntfschr@chromium.org>
Reviewed-by: default avatarRichard Coles <torne@chromium.org>
Cr-Commit-Position: refs/heads/master@{#726993}
parent 606e6049
......@@ -495,6 +495,7 @@ android_library("common_java") {
sources = [
"java/src/org/chromium/android_webview/common/AwResource.java",
"java/src/org/chromium/android_webview/common/Flag.java",
"java/src/org/chromium/android_webview/common/FlagOverrideConstants.java",
"java/src/org/chromium/android_webview/common/FlagOverrideHelper.java",
"java/src/org/chromium/android_webview/common/ProductionSupportedFlagList.java",
"java/src/org/chromium/android_webview/common/services/ServiceNames.java",
......
......@@ -168,6 +168,10 @@ public class WebViewChromiumAwInit {
// available when AwFeatureListCreator::SetUpFieldTrials() runs.
finishVariationsInitLocked();
if (AwBrowserProcess.isDeveloperModeEnabled()) {
AwBrowserProcess.getAndApplyFlagOverridesSync();
}
AwBrowserProcess.start();
AwBrowserProcess.handleMinidumpsAndSetMetricsConsent(true /* updateMetricsConsent */);
......
......@@ -8,6 +8,9 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
......@@ -15,7 +18,10 @@ import android.os.RemoteException;
import android.os.StrictMode;
import org.chromium.android_webview.common.CommandLineUtil;
import org.chromium.android_webview.common.FlagOverrideConstants;
import org.chromium.android_webview.common.FlagOverrideHelper;
import org.chromium.android_webview.common.PlatformServiceBridge;
import org.chromium.android_webview.common.ProductionSupportedFlagList;
import org.chromium.android_webview.common.services.ICrashReceiverService;
import org.chromium.android_webview.common.services.ServiceNames;
import org.chromium.android_webview.metrics.AwMetricsServiceClient;
......@@ -46,6 +52,7 @@ import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
......@@ -364,6 +371,47 @@ public final class AwBrowserProcess {
});
}
// Quickly determine whether developer mode is enabled.
public static boolean isDeveloperModeEnabled() {
final Context context = ContextUtils.getApplicationContext();
ComponentName flagOverrideContentProvider = new ComponentName(
getWebViewPackageName(), ServiceNames.FLAG_OVERRIDE_CONTENT_PROVIDER);
int enabledState =
context.getPackageManager().getComponentEnabledSetting(flagOverrideContentProvider);
return enabledState == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
}
public static void getAndApplyFlagOverridesSync() {
FlagOverrideHelper helper = new FlagOverrideHelper(ProductionSupportedFlagList.sFlagList);
helper.applyFlagOverrides(getFlagOverrides());
}
private static Map<String, Boolean> getFlagOverrides() {
Map<String, Boolean> flagOverrides = new HashMap<>();
Uri uri = new Uri.Builder()
.scheme("content")
.authority(getWebViewPackageName()
+ FlagOverrideConstants.URI_AUTHORITY_SUFFIX)
.path(FlagOverrideConstants.URI_PATH)
.build();
final Context appContext = ContextUtils.getApplicationContext();
try (Cursor cursor = appContext.getContentResolver().query(uri, /* projection */ null,
/* selection */ null, /* selectionArgs */ null, /* sortOrder */ null)) {
assert cursor != null : "ContentProvider doesn't support querying '" + uri + "'";
int flagNameColumnIndex =
cursor.getColumnIndexOrThrow(FlagOverrideConstants.FLAG_NAME_COLUMN);
int flagStateColumnIndex =
cursor.getColumnIndexOrThrow(FlagOverrideConstants.FLAG_STATE_COLUMN);
while (cursor.moveToNext()) {
String flagName = cursor.getString(flagNameColumnIndex);
boolean flagState = cursor.getInt(flagStateColumnIndex) != 0;
flagOverrides.put(flagName, flagState);
}
}
return flagOverrides;
}
// Do not instantiate this class.
private AwBrowserProcess() {}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.android_webview.common;
/**
* Constants to facilitate communication with {@code FlagOverrideContentProvider}.
*/
public final class FlagOverrideConstants {
// Do not instantiate this class.
private FlagOverrideConstants() {}
public static final String URI_AUTHORITY_SUFFIX = ".FlagOverrideContentProvider";
public static final String URI_PATH = "/flag-overrides";
public static final String FLAG_NAME_COLUMN = "flagName";
public static final String FLAG_STATE_COLUMN = "flagState";
}
......@@ -17,6 +17,8 @@ public class ServiceNames {
"org.chromium.android_webview.services.CrashReceiverService";
public static final String DEVELOPER_UI_SERVICE =
"org.chromium.android_webview.services.DeveloperUiService";
public static final String FLAG_OVERRIDE_CONTENT_PROVIDER =
"org.chromium.android_webview.services.FlagOverrideContentProvider";
public static final String VARIATIONS_SEED_SERVER =
"org.chromium.android_webview.services.VariationsSeedServer";
......
......@@ -15,6 +15,7 @@ import org.chromium.android_webview.common.services.ServiceNames;
import org.chromium.android_webview.services.AwMinidumpUploadJobService;
import org.chromium.android_webview.services.CrashReceiverService;
import org.chromium.android_webview.services.DeveloperUiService;
import org.chromium.android_webview.services.FlagOverrideContentProvider;
import org.chromium.android_webview.services.VariationsSeedServer;
import org.chromium.testing.local.LocalRobolectricTestRunner;
......@@ -32,6 +33,9 @@ public class ServiceNamesTest {
ServiceNames.CRASH_RECEIVER_SERVICE);
Assert.assertEquals("Incorrect class name constant", DeveloperUiService.class.getName(),
ServiceNames.DEVELOPER_UI_SERVICE);
Assert.assertEquals("Incorrect class name constant",
FlagOverrideContentProvider.class.getName(),
ServiceNames.FLAG_OVERRIDE_CONTENT_PROVIDER);
Assert.assertEquals("Incorrect class name constant", VariationsSeedServer.class.getName(),
ServiceNames.VARIATIONS_SEED_SERVER);
}
......
......@@ -73,6 +73,7 @@ android_library("services_java") {
"java/src/org/chromium/android_webview/services/AwVariationsSeedFetcher.java",
"java/src/org/chromium/android_webview/services/CrashReceiverService.java",
"java/src/org/chromium/android_webview/services/DeveloperUiService.java",
"java/src/org/chromium/android_webview/services/FlagOverrideContentProvider.java",
"java/src/org/chromium/android_webview/services/VariationsSeedHolder.java",
"java/src/org/chromium/android_webview/services/VariationsSeedServer.java",
]
......
......@@ -72,6 +72,12 @@
android:exported="true"
android:authorities="{{ manifest_package }}.LicenseContentProvider"
android:process=":webview_apk" /> {# Explicit process required for monochrome compatibility. #}
<!-- Disabled by default, enabled at runtime by Developer UI. -->
<provider android:name="org.chromium.android_webview.services.FlagOverrideContentProvider"
android:exported="true"
android:enabled="false"
android:authorities="{{ manifest_package }}.FlagOverrideContentProvider"
android:process=":webview_service" /> {# Explicit process required for monochrome compatibility. #}
{% if donor_package is not defined %}
<!-- If you change the variations services, also see
android_webview/test/shell/AndroidManifest.xml. -->
......@@ -92,10 +98,8 @@
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"
android:process=":webview_service" /> {# Explicit process required for monochrome compatibility. #}
<!-- Disabled by default, enabled at runtime by Developer UI. -->
<service android:name="org.chromium.android_webview.services.DeveloperUiService"
android:exported="true"
android:enabled="false"
android:exported="false"
android:process=":webview_service" /> {# Explicit process required for monochrome compatibility. #}
{% endif %}
{% endmacro %}
......
......@@ -10,7 +10,6 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
......@@ -192,7 +191,6 @@ public class FlagsActivity extends Activity {
private class FlagsServiceConnection implements ServiceConnection {
public void start() {
enableDeveloperMode();
Intent intent = new Intent();
intent.setClassName(
FlagsActivity.this.getPackageName(), ServiceNames.DEVELOPER_UI_SERVICE);
......@@ -226,13 +224,6 @@ public class FlagsActivity extends Activity {
connection.start();
}
private void enableDeveloperMode() {
ComponentName developerModeService =
new ComponentName(this, ServiceNames.DEVELOPER_UI_SERVICE);
this.getPackageManager().setComponentEnabledSetting(developerModeService,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
}
private void resetAllFlags() {
// Clear the map, then update the Spinners from the map value.
mOverriddenFlags.clear();
......
......@@ -32,13 +32,11 @@ public final class DeveloperUiService extends Service {
private static final String CHANNEL_ID = "DevUiChannel";
private static final int FLAG_OVERRIDE_NOTIFICATION_ID = 1;
private final Object mLock = new Object();
// TODO(ntfschr): at the moment we're only writing to this map. When we implement the
// WebView-side implementation, we'll read the map to send the flag overrides.
@GuardedBy("mLock")
private Map<String, Boolean> mOverriddenFlags = new HashMap<>();
private static final Object sLock = new Object();
@GuardedBy("sLock")
private static Map<String, Boolean> sOverriddenFlags = new HashMap<>();
@GuardedBy("mLock")
@GuardedBy("sLock")
private boolean mDeveloperModeEnabled;
private final IDeveloperUiService.Stub mBinder = new IDeveloperUiService.Stub() {
......@@ -48,9 +46,9 @@ public final class DeveloperUiService extends Service {
throw new SecurityException(
"setFlagOverrides() may only be called by the Developer UI app");
}
synchronized (mLock) {
mOverriddenFlags = overriddenFlags;
if (mOverriddenFlags.isEmpty()) {
synchronized (sLock) {
sOverriddenFlags = overriddenFlags;
if (sOverriddenFlags.isEmpty()) {
disableDeveloperMode();
} else {
enableDeveloperMode();
......@@ -59,6 +57,14 @@ public final class DeveloperUiService extends Service {
}
};
public static Map<String, Boolean> getFlagOverrides() {
synchronized (sLock) {
// Create a copy so the caller can do what it wants with the Map without worrying about
// thread safety.
return new HashMap<>(sOverriddenFlags);
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
......@@ -118,28 +124,34 @@ public final class DeveloperUiService extends Service {
}
private void enableDeveloperMode() {
synchronized (mLock) {
synchronized (sLock) {
if (mDeveloperModeEnabled) return;
// Keep this service alive as long as we're in developer mode.
startService(new Intent(this, DeveloperUiService.class));
markAsForegroundService();
ComponentName flagOverrideContentProvider =
new ComponentName(this, FlagOverrideContentProvider.class.getName());
getPackageManager().setComponentEnabledSetting(flagOverrideContentProvider,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
mDeveloperModeEnabled = true;
}
}
private void disableDeveloperMode() {
synchronized (mLock) {
synchronized (sLock) {
if (!mDeveloperModeEnabled) return;
stopForeground(/* removeNotification */ true);
mDeveloperModeEnabled = false;
ComponentName developerModeService =
new ComponentName(this, DeveloperUiService.class.getName());
getPackageManager().setComponentEnabledSetting(developerModeService,
ComponentName flagOverrideContentProvider =
new ComponentName(this, FlagOverrideContentProvider.class.getName());
getPackageManager().setComponentEnabledSetting(flagOverrideContentProvider,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
// Finally, stop the service explicitly. Do this last to make sure we do the other
// necessary cleanup.
stopForeground(/* removeNotification */ true);
stopSelf();
}
}
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.android_webview.services;
import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import org.chromium.android_webview.common.FlagOverrideConstants;
import java.util.Map;
/**
* A {@link ContentProvider} to fetch the flag overrides, via the {@code query()} method. No special
* permissions are required to access this ContentProvider, and it can be accessed by any context
* (including the embedded WebView implementation).
*/
public final class FlagOverrideContentProvider extends ContentProvider {
@Override
public boolean onCreate() {
return true;
}
@Override
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
throw new UnsupportedOperationException();
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException();
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException();
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
if (FlagOverrideConstants.URI_PATH.equals(uri.getPath())) {
Map<String, Boolean> flagOverrides = DeveloperUiService.getFlagOverrides();
final String[] columns = {FlagOverrideConstants.FLAG_NAME_COLUMN,
FlagOverrideConstants.FLAG_STATE_COLUMN};
MatrixCursor cursor = new MatrixCursor(columns, flagOverrides.size());
for (Map.Entry<String, Boolean> entry : flagOverrides.entrySet()) {
String flagName = entry.getKey();
boolean enabled = entry.getValue();
cursor.addRow(new Object[] {flagName, enabled ? 1 : 0});
}
if (flagOverrides.isEmpty()) {
disableDeveloperMode();
}
return cursor;
}
return null;
}
private void disableDeveloperMode() {
ComponentName flagOverrideContentProvider =
new ComponentName(getContext(), FlagOverrideContentProvider.class.getName());
getContext().getPackageManager().setComponentEnabledSetting(flagOverrideContentProvider,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
// Stop the service explicitly, in case it's running. NOOP if the service is not running.
getContext().stopService(new Intent(getContext(), DeveloperUiService.class));
}
@Override
public String getType(Uri uri) {
throw new UnsupportedOperationException();
}
}
......@@ -1323,6 +1323,12 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
<provider
android:authorities="org.chromium.chrome.FlagOverrideContentProvider"
android:enabled="false"
android:exported="true"
android:name="org.chromium.android_webview.services.FlagOverrideContentProvider"
android:process=":webview_service"/>
<provider
android:authorities="org.chromium.chrome.LicenseContentProvider"
android:exported="true"
......@@ -1489,11 +1495,6 @@
android:isolatedProcess="true"
android:name="org.chromium.chrome.browser.photo_picker.DecoderService"
android:process=":decoder_service"/>
<service
android:enabled="false"
android:exported="true"
android:name="org.chromium.android_webview.services.DeveloperUiService"
android:process=":webview_service"/>
<service
android:exported="false"
android:isolatedProcess="false"
......@@ -1548,6 +1549,10 @@
android:name="org.chromium.android_webview.services.AwVariationsSeedFetcher"
android:permission="android.permission.BIND_JOB_SERVICE"
android:process=":webview_service"/>
<service
android:exported="false"
android:name="org.chromium.android_webview.services.DeveloperUiService"
android:process=":webview_service"/>
<service
android:exported="false"
android:name="org.chromium.chrome.browser.bookmarkswidget.BookmarkWidgetService"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment