Commit acd74a0c authored by Nate Fischer's avatar Nate Fischer Committed by Commit Bot

AW: implement developer UI service

No change to production behavior.

This extends https://crrev.com/c/1910811 by sending the toggled flag
state to a central service (which will, in a later CL, distribute this
information to the WebView implementation running within apps).

For simplicity, both the UI and the service maintain copies of the full
flag override state (rather than the UI sending only updates).

Bug: 981143
Test: run_android_webview_junit_tests -f *ServiceNamesTest.*
Change-Id: I8644c730a9641fe44d6177bca8019dd8060ca743
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1923500Reviewed-by: default avatarRobert Sesek <rsesek@chromium.org>
Reviewed-by: default avatarRichard Coles <torne@chromium.org>
Reviewed-by: default avatarHazem Ashmawy <hazems@chromium.org>
Commit-Queue: Nate Fischer <ntfschr@chromium.org>
Cr-Commit-Position: refs/heads/master@{#718340}
parent cae4b4a6
......@@ -765,6 +765,7 @@ android_aidl("common_aidl") {
import_include = [ "java/src" ]
sources = [
"java/src/org/chromium/android_webview/common/services/ICrashReceiverService.aidl",
"java/src/org/chromium/android_webview/common/services/IDeveloperUiService.aidl",
"java/src/org/chromium/android_webview/common/services/IVariationsSeedServer.aidl",
]
}
......
......@@ -29,4 +29,7 @@ public final class ProductionSupportedFlagList {
Flag.commandLine("webview-log-js-console-messages",
"Mirrors JavaScript console messages to system logs."),
};
// TODO(ntfschr): add a method to map String to Flag instance, when we support retrieving flags
// from the service.
}
// 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.services;
interface IDeveloperUiService {
// Override flag values. |overriddenFlags| is a Map<String, Boolean>, where the boolean value
// indicates whether to enable or disable the flag named by the string value. Strings can be
// converted to org.chromium.android_webview.common.Flag via ProductionSupportedFlagList.
//
// This may only be called from the developer UI app itself. Calling this from any other context
// (ex. the embedded WebView implementation) will throw a SecurityException.
void setFlagOverrides(in Map overriddenFlags);
}
......@@ -13,6 +13,8 @@ package org.chromium.android_webview.common.services;
public class ServiceNames {
public static final String CRASH_RECEIVER_SERVICE =
"org.chromium.android_webview.services.CrashReceiverService";
public static final String DEVELOPER_UI_SERVICE =
"org.chromium.android_webview.services.DeveloperUiService";
public static final String VARIATIONS_SEED_SERVER =
"org.chromium.android_webview.services.VariationsSeedServer";
......
......@@ -13,6 +13,7 @@ import org.robolectric.annotation.Config;
import org.chromium.android_webview.common.services.ServiceNames;
import org.chromium.android_webview.services.CrashReceiverService;
import org.chromium.android_webview.services.DeveloperUiService;
import org.chromium.android_webview.services.VariationsSeedServer;
import org.chromium.testing.local.LocalRobolectricTestRunner;
......@@ -25,6 +26,8 @@ public class ServiceNamesTest {
public void testServiceNamesValid() {
Assert.assertEquals("Incorrect class name constant", CrashReceiverService.class.getName(),
ServiceNames.CRASH_RECEIVER_SERVICE);
Assert.assertEquals("Incorrect class name constant", DeveloperUiService.class.getName(),
ServiceNames.DEVELOPER_UI_SERVICE);
Assert.assertEquals("Incorrect class name constant", VariationsSeedServer.class.getName(),
ServiceNames.VARIATIONS_SEED_SERVER);
}
......
......@@ -52,6 +52,7 @@ android_library("devui_java") {
deps = [
":devui_resources",
":system_webview_manifest",
"//android_webview:common_aidl_java",
"//android_webview:common_commandline_java",
"//android_webview:common_crash_java",
"//android_webview:common_java",
......@@ -71,6 +72,7 @@ android_library("services_java") {
"java/src/org/chromium/android_webview/services/AwMinidumpUploaderDelegate.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/VariationsSeedHolder.java",
"java/src/org/chromium/android_webview/services/VariationsSeedServer.java",
]
......@@ -79,6 +81,7 @@ android_library("services_java") {
"//android_webview:common_aidl_java",
"//android_webview:common_commandline_java",
"//android_webview:common_crash_java",
"//android_webview:common_java",
"//android_webview:common_platform_services_java",
"//android_webview:common_variations_utils_java",
"//base:base_java",
......
......@@ -18,6 +18,9 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Required by WebView Developer UI only -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application android:label="{{ application_label|default('Android System WebView') }}"
android:icon="@{{manifest_package|default('com.android.webview')}}:drawable/icon_webview"
android:name="{{ application_name|default('org.chromium.android_webview.nonembedded.WebViewApkApplication') }}"
......@@ -89,6 +92,11 @@
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:process=":webview_service" /> {# Explicit process required for monochrome compatibility. #}
{% endif %}
{% endmacro %}
{{ common(manifest_package|default('com.android.webview'), library|default('libwebviewchromium.so')) }}
......
......@@ -6,7 +6,14 @@ package org.chromium.android_webview.devui;
import android.annotation.SuppressLint;
import android.app.Activity;
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;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
......@@ -19,27 +26,32 @@ import android.widget.TextView;
import org.chromium.android_webview.common.Flag;
import org.chromium.android_webview.common.ProductionSupportedFlagList;
import org.chromium.android_webview.common.services.IDeveloperUiService;
import org.chromium.android_webview.common.services.ServiceNames;
import org.chromium.android_webview.devui.util.NavigationMenuHelper;
import org.chromium.base.Log;
import java.util.Arrays;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.Map;
/**
* An activity to toggle experimental WebView flags/features.
*/
@SuppressLint("SetTextI18n")
public class FlagsActivity extends Activity {
// TODO(ntfschr): at the moment we're only writing to these sets. When we implement the service,
// we'll also read the contents to know what to send to embedded WebViews.
private final Set<Flag> mEnabledFlags = new HashSet<>();
private final Set<Flag> mDisabledFlags = new HashSet<>();
private static final String TAG = "WebViewDevTools";
private final Map<String, Boolean> mOverriddenFlags = new HashMap<>();
private static final String STATE_DEFAULT = "Default";
private static final String STATE_ENABLED = "Enabled";
private static final String STATE_DISABLED = "Disabled";
private static final String[] sFlagStates = {
"Default",
"Enabled",
"Disabled",
STATE_DEFAULT,
STATE_ENABLED,
STATE_DISABLED,
};
private WebViewPackageError mDifferentPackageError;
......@@ -63,6 +75,10 @@ public class FlagsActivity extends Activity {
new WebViewPackageError(this, findViewById(R.id.flags_activity_layout));
// show the dialog once when the activity is created.
mDifferentPackageError.showDialogIfDifferent();
// TODO(ntfschr): once there's a way to get the flag overrides out of the service, we should
// repopulate the UI based on that data (otherwise, we send an empty map to the service,
// which causes the service to stop itself).
}
@Override
......@@ -84,22 +100,18 @@ public class FlagsActivity extends Activity {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
switch (sFlagStates[position]) {
case "Default":
mEnabledFlags.remove(mFlag);
mDisabledFlags.remove(mFlag);
case STATE_DEFAULT:
mOverriddenFlags.remove(mFlag.getName());
break;
case "Enabled":
mEnabledFlags.add(mFlag);
mDisabledFlags.remove(mFlag);
case STATE_ENABLED:
mOverriddenFlags.put(mFlag.getName(), true);
break;
case "Disabled":
mEnabledFlags.remove(mFlag);
mDisabledFlags.add(mFlag);
case STATE_DISABLED:
mOverriddenFlags.put(mFlag.getName(), false);
break;
}
// TODO(ntfschr): enable/disable enable) developer mode (when that's supported), based
// on whether (mEnabledFlags.isEmpty() && mDisabledFlags.isEmpty()).
sendFlagsToService();
}
@Override
......@@ -155,4 +167,47 @@ public class FlagsActivity extends Activity {
}
return super.onOptionsItemSelected(item);
}
private class FlagsServiceConnection implements ServiceConnection {
public void start() {
enableDeveloperMode();
Intent intent = new Intent();
intent.setClassName(
FlagsActivity.this.getPackageName(), ServiceNames.DEVELOPER_UI_SERVICE);
if (!FlagsActivity.this.bindService(intent, this, Context.BIND_AUTO_CREATE)) {
Log.e(TAG, "Failed to bind to Developer UI service");
}
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Intent intent = new Intent();
intent.setClassName(
FlagsActivity.this.getPackageName(), ServiceNames.DEVELOPER_UI_SERVICE);
try {
IDeveloperUiService.Stub.asInterface(service).setFlagOverrides(mOverriddenFlags);
} catch (RemoteException e) {
Log.e(TAG, "Failed to send flag overrides to service", e);
} finally {
// Unbind when we've sent the flags overrides, since we can always rebind later. The
// service will manage its own lifetime.
FlagsActivity.this.unbindService(this);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {}
}
private void sendFlagsToService() {
FlagsServiceConnection connection = new FlagsServiceConnection();
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);
}
}
// 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.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.Process;
import org.chromium.android_webview.common.services.IDeveloperUiService;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.concurrent.GuardedBy;
/**
* A Service to support Developer UI features. This service enables communication between the
* WebView implementation embedded in apps on the system and the Developer UI.
*/
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<>();
@GuardedBy("mLock")
private boolean mDeveloperModeEnabled;
private final IDeveloperUiService.Stub mBinder = new IDeveloperUiService.Stub() {
@Override
public void setFlagOverrides(Map overriddenFlags) {
if (Binder.getCallingUid() != Process.myUid()) {
throw new SecurityException(
"setFlagOverrides() may only be called by the Developer UI app");
}
synchronized (mLock) {
mOverriddenFlags = overriddenFlags;
if (mOverriddenFlags.isEmpty()) {
disableDeveloperMode();
} else {
enableDeveloperMode();
}
}
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@TargetApi(Build.VERSION_CODES.O)
private Notification.Builder createNotificationBuilder() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return new Notification.Builder(this, CHANNEL_ID);
}
return new Notification.Builder(this);
}
@TargetApi(Build.VERSION_CODES.O)
private void registerDefaultNotificationChannel() {
assert Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
CharSequence name = "WebView DevTools alerts";
// The channel importance should be consistent with the Notification priority on pre-O.
NotificationChannel channel =
new NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
channel.enableVibration(false);
channel.enableLights(false);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
private void markAsForegroundService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
registerDefaultNotificationChannel();
}
Intent notificationIntent = new Intent();
notificationIntent.setClassName(
getPackageName(), "org.chromium.android_webview.devui.FlagsActivity");
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
Notification.Builder builder =
createNotificationBuilder()
.setContentTitle("WARNING: experimental WebView features enabled")
.setContentText("Tap to see experimental features.")
.setSmallIcon(android.R.drawable.stat_notify_error)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setTicker("Experimental WebView features enabled");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
builder = builder
// No sound, vibration, or lights.
.setDefaults(0)
// This should be consistent with NotificationChannel importance.
.setPriority(Notification.PRIORITY_LOW);
}
Notification notification = builder.build();
startForeground(FLAG_OVERRIDE_NOTIFICATION_ID, notification);
}
private void enableDeveloperMode() {
synchronized (mLock) {
if (mDeveloperModeEnabled) return;
// Keep this service alive as long as we're in developer mode.
startService(new Intent(this, DeveloperUiService.class));
markAsForegroundService();
mDeveloperModeEnabled = true;
}
}
private void disableDeveloperMode() {
synchronized (mLock) {
if (!mDeveloperModeEnabled) return;
stopForeground(/* removeNotification */ true);
mDeveloperModeEnabled = false;
ComponentName developerModeService =
new ComponentName(this, DeveloperUiService.class.getName());
getPackageManager().setComponentEnabledSetting(developerModeService,
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.
stopSelf();
}
}
}
......@@ -1485,6 +1485,11 @@
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"
......
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