Skip to content

Commit

Permalink
AW: fetch flag overrides via ContentProvider
Browse files Browse the repository at this point in the history
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: Richard Coles <torne@chromium.org>
Cr-Commit-Position: refs/heads/master@{#726993}
  • Loading branch information
ntfschr-chromium authored and Commit Bot committed Dec 21, 2019
1 parent 606e604 commit 34df84d
Show file tree
Hide file tree
Showing 12 changed files with 202 additions and 32 deletions.
1 change: 1 addition & 0 deletions android_webview/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ protected void startChromiumLocked() {
// available when AwFeatureListCreator::SetUpFieldTrials() runs.
finishVariationsInitLocked();

if (AwBrowserProcess.isDeveloperModeEnabled()) {
AwBrowserProcess.getAndApplyFlagOverridesSync();
}

AwBrowserProcess.start();
AwBrowserProcess.handleMinidumpsAndSetMetricsConsent(true /* updateMetricsConsent */);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@
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;
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;
Expand Down Expand Up @@ -46,6 +52,7 @@
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;

Expand Down Expand Up @@ -364,6 +371,47 @@ public void onServiceDisconnected(ComponentName className) {}
});
}

// 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() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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;

Expand All @@ -32,6 +33,9 @@ public void testServiceNamesValid() {
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);
}
Expand Down
1 change: 1 addition & 0 deletions android_webview/nonembedded/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
10 changes: 7 additions & 3 deletions android_webview/nonembedded/java/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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. -->
Expand All @@ -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 %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
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;
Expand Down Expand Up @@ -192,7 +191,6 @@ public boolean onOptionsItemSelected(MenuItem item) {

private class FlagsServiceConnection implements ServiceConnection {
public void start() {
enableDeveloperMode();
Intent intent = new Intent();
intent.setClassName(
FlagsActivity.this.getPackageName(), ServiceNames.DEVELOPER_UI_SERVICE);
Expand Down Expand Up @@ -226,13 +224,6 @@ private void sendFlagsToService() {
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -48,9 +46,9 @@ public void setFlagOverrides(Map overriddenFlags) {
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();
Expand All @@ -59,6 +57,14 @@ public void setFlagOverrides(Map overriddenFlags) {
}
};

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;
Expand Down Expand Up @@ -118,28 +124,34 @@ private void markAsForegroundService() {
}

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();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// 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();
}
}
Loading

0 comments on commit 34df84d

Please sign in to comment.