Unverified Commit e1f4cfb4 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] add toggle `b` and service extension to change platform brightness (#59571)

A frequent request from the last Flutter developer survey was for an easier method of testing light/dark mode changes. Currently, a user needs to manually change the theme settings or adjust phone settings to see the difference. Instead we should add a toggle from the CLI, and eventually devtools/Intellij/Vscode that allows developers to override the current setting.

Fixes #59495

Adds flutter.ext.brightnessOverride service protocol which either queries the current platform brightness, or overrides it to a new value. This accepts either Brightness.light or Brightness.dark as a value.

Adds a CLI toggle b which allows the setting to be toggled manually.

Requires an update to the MediaQuery, to conditionally use a debug override when not in release mode
parent f39ab522
......@@ -8,7 +8,7 @@ import 'dart:async';
import 'dart:convert' show json;
import 'dart:developer' as developer;
import 'dart:io' show exit;
import 'dart:ui' as ui show saveCompilationTrace, Window, window;
import 'dart:ui' as ui show Window, window, Brightness;
// Before adding any more dart:ui imports, please read the README.
import 'package:meta/meta.dart';
......@@ -143,14 +143,6 @@ abstract class BindingBase {
name: 'exit',
callback: _exitApplication,
);
registerServiceExtension(
name: 'saveCompilationTrace',
callback: (Map<String, String> parameters) async {
return <String, dynamic>{
'value': ui.saveCompilationTrace(),
};
},
);
}
assert(() {
......@@ -195,6 +187,33 @@ abstract class BindingBase {
};
},
);
const String brightnessOverrideExtensionName = 'brightnessOverride';
registerServiceExtension(
name: brightnessOverrideExtensionName,
callback: (Map<String, String> parameters) async {
if (parameters.containsKey('value')) {
switch (parameters['value']) {
case 'Brightness.light':
debugBrightnessOverride = ui.Brightness.light;
break;
case 'Brightness.dark':
debugBrightnessOverride = ui.Brightness.dark;
break;
default:
debugBrightnessOverride = null;
}
_postExtensionStateChangedEvent(
brightnessOverrideExtensionName,
(debugBrightnessOverride ?? window.platformBrightness).toString(),
);
await reassembleApplication();
}
return <String, dynamic>{
'value': (debugBrightnessOverride ?? window.platformBrightness).toString(),
};
},
);
return true;
}());
assert(() {
......
......@@ -5,6 +5,7 @@
// @dart = 2.8
import 'dart:async';
import 'dart:ui' as ui show Brightness;
import 'assertions.dart';
import 'platform.dart';
......@@ -27,7 +28,8 @@ bool debugAssertAllFoundationVarsUnset(String reason, { DebugPrintCallback debug
assert(() {
if (debugPrint != debugPrintOverride ||
debugDefaultTargetPlatformOverride != null ||
debugDoublePrecision != null)
debugDoublePrecision != null ||
debugBrightnessOverride != null)
throw FlutterError(reason);
return true;
}());
......@@ -99,3 +101,12 @@ String debugFormatDouble(double value) {
}
return value.toStringAsFixed(1);
}
/// A setting that can be used to override the platform [Brightness] exposed
/// from [BindingBase.window].
///
/// See also:
///
/// * [WidgetsApp], which uses the [debugBrightnessOverride] setting in debug mode
/// to construct a [MediaQueryData].
ui.Brightness debugBrightnessOverride;
......@@ -1474,8 +1474,12 @@ class _MediaQueryFromWindowsState extends State<_MediaQueryFromWindow> with Widg
@override
Widget build(BuildContext context) {
MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window);
if (!kReleaseMode) {
data = data.copyWith(platformBrightness: debugBrightnessOverride);
}
return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
data: data,
child: widget.child,
);
}
......
......@@ -167,9 +167,8 @@ void main() {
// The following service extensions are disabled in web:
// 1. exit
// 2. saveCompilationTrace
// 3. showPerformanceOverlay
const int disabledExtensions = kIsWeb ? 3 : 0;
// 2. showPerformanceOverlay
const int disabledExtensions = kIsWeb ? 2 : 0;
// If you add a service extension... TEST IT! :-)
// ...then increment this number.
expect(binding.extensions.length, 28 + widgetInspectorExtensionCount - disabledExtensions);
......@@ -711,15 +710,13 @@ void main() {
expect(binding.frameScheduled, isFalse);
});
test('Service extensions - saveCompilationTrace', () async {
test('Service extensions - brightnessOverride', () async {
Map<String, dynamic> result;
result = await binding.testExtension('saveCompilationTrace', <String, String>{});
final String trace = String.fromCharCodes((result['value'] as List<dynamic>).cast<int>());
expect(trace, contains('dart:core,Object,Object.\n'));
expect(trace, contains('package:test_api/test_api.dart,::,test\n'));
expect(trace, contains('service_extensions_test.dart,::,main\n'));
}, skip: isBrowser); // Compilation trace is Dart VM specific and not
// supported in browsers.
result = await binding.testExtension('brightnessOverride', <String, String>{});
final String brightnessValue = result['value'] as String;
expect(brightnessValue, 'Brightness.light');
});
test('Service extensions - fastReassemble', () async {
Map<String, dynamic> result;
......
......@@ -8,7 +8,6 @@ import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mockito/mockito.dart';
class StateMarker extends StatefulWidget {
const StateMarker({ Key key, this.child }) : super(key: key);
......@@ -837,4 +836,22 @@ void main() {
});
}
class MockAccessibilityFeature extends Mock implements AccessibilityFeatures {}
class MockAccessibilityFeature implements AccessibilityFeatures {
@override
bool get accessibleNavigation => true;
@override
bool get boldText => true;
@override
bool get disableAnimations => true;
@override
bool get highContrast => true;
@override
bool get invertColors => true;
@override
bool get reduceMotion => true;
}
......@@ -76,6 +76,13 @@ class CommandHelp {
'debugProfileWidgetBuilds',
);
CommandHelpOption _b;
CommandHelpOption get b => _b ??= _makeOption(
'b',
'Toggle the platform brightness setting (dark and light mode).',
'debugBrightnessOverride',
);
CommandHelpOption _c;
CommandHelpOption get c => _c ??= _makeOption(
'c',
......
......@@ -264,6 +264,30 @@ abstract class ResidentWebRunner extends ResidentRunner {
}
}
@override
Future<void> debugToggleBrightness() async {
try {
final Brightness currentBrightness = await _vmService
?.flutterBrightnessOverride(
isolateId: null,
);
Brightness next;
if (currentBrightness == Brightness.light) {
next = Brightness.dark;
} else if (currentBrightness == Brightness.dark) {
next = Brightness.light;
}
next = await _vmService
?.flutterBrightnessOverride(
brightness: next,
isolateId: null,
);
globals.logger.printStatus('Changed brightness to $next.');
} on vmservice.RPCError {
return;
}
}
@override
Future<void> stopEchoingDeviceLog() async {
// Do nothing for ResidentWebRunner
......
......@@ -425,6 +425,24 @@ class FlutterDevice {
}
}
Future<Brightness> toggleBrightness({ Brightness current }) async {
final List<FlutterView> views = await vmService.getFlutterViews();
Brightness next;
if (current == Brightness.light) {
next = Brightness.dark;
} else if (current == Brightness.dark) {
next = Brightness.light;
}
for (final FlutterView view in views) {
next = await vmService.flutterBrightnessOverride(
isolateId: view.uiIsolate.id,
brightness: next,
);
}
return next;
}
Future<String> togglePlatform({ String from }) async {
final List<FlutterView> views = await vmService.getFlutterViews();
final String to = nextPlatform(from, featureFlags);
......@@ -973,6 +991,17 @@ abstract class ResidentRunner {
}
}
Future<void> debugToggleBrightness() async {
final Brightness brightness = await flutterDevices.first.toggleBrightness();
Brightness next;
for (final FlutterDevice device in flutterDevices) {
next = await device.toggleBrightness(
current: brightness,
);
globals.logger.printStatus('Changed brightness to $next.');
}
}
/// Take a screenshot on the provided [device].
///
/// If the device has a connected vmservice, this method will attempt to hide
......@@ -1221,6 +1250,7 @@ abstract class ResidentRunner {
commandHelp.s.print();
}
if (supportsServiceProtocol) {
commandHelp.b.print();
commandHelp.w.print();
commandHelp.t.print();
if (isRunningDebug) {
......@@ -1362,6 +1392,9 @@ class TerminalHandler {
return true;
}
return false;
case 'b':
await residentRunner.debugToggleBrightness();
return true;
case 'c':
residentRunner.clearScreen();
return true;
......
......@@ -708,6 +708,30 @@ extension FlutterVmService on vm_service.VmService {
return 'unknown';
}
/// Return the current brightness value for the flutter view running with
/// the main isolate [isolateId].
///
/// If a non-null value is provided for [brightness], the brightness override
/// is updated with this value.
Future<Brightness> flutterBrightnessOverride({
Brightness brightness,
@required String isolateId,
}) async {
final Map<String, dynamic> result = await invokeFlutterExtensionRpcRaw(
'ext.flutter.brightnessOverride',
isolateId: isolateId,
args: brightness != null
? <String, dynamic>{'value': brightness.toString()}
: <String, String>{},
);
if (result != null && result['value'] is String) {
return (result['value'] as String) == 'Brightness.light'
? Brightness.light
: Brightness.dark;
}
return null;
}
/// Invoke a flutter extension method, if the flutter extension is not
/// available, returns null.
Future<Map<String, dynamic>> invokeFlutterExtensionRpcRaw(
......@@ -860,3 +884,19 @@ Future<String> sharedSkSlWriter(Device device, Map<String, Object> data, {
globals.logger.printStatus('Wrote SkSL data to ${outputFile.path}.');
return outputFile.path;
}
/// A brightness enum that matches the values https://github.com/flutter/engine/blob/3a96741247528133c0201ab88500c0c3c036e64e/lib/ui/window.dart#L1328
/// Describes the contrast of a theme or color palette.
enum Brightness {
/// The color is dark and will require a light text color to achieve readable
/// contrast.
///
/// For example, the color might be dark grey, requiring white text.
dark,
/// The color is light and will require a dark text color to achieve readable
/// contrast.
///
/// For example, the color might be bright white, requiring black text.
light,
}
......@@ -700,6 +700,7 @@ void main() {
commandHelp.c,
commandHelp.q,
commandHelp.s,
commandHelp.b,
commandHelp.w,
commandHelp.t,
commandHelp.L,
......@@ -1096,6 +1097,36 @@ void main() {
verify(mockFlutterDevice.toggleDebugPaintSizeEnabled()).called(1);
}));
testUsingContext('ResidentRunner debugToggleBrightness calls flutter device', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
await residentRunner.debugToggleBrightness();
verify(mockFlutterDevice.toggleBrightness()).called(2);
}));
testUsingContext('FlutterDevice.toggleBrightness invokes correct VM service request', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
const FakeVmServiceRequest(
method: 'ext.flutter.brightnessOverride',
args: <String, Object>{
'isolateId': '1',
},
jsonResponse: <String, Object>{
'value': 'Brightness.dark'
},
),
]);
final FlutterDevice device = FlutterDevice(
mockDevice,
buildInfo: BuildInfo.debug,
);
device.vmService = fakeVmServiceHost.vmService;
expect(await device.toggleBrightness(), Brightness.dark);
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}));
testUsingContext('ResidentRunner debugToggleDebugCheckElevationsEnabled calls flutter device', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
await residentRunner.debugToggleDebugCheckElevationsEnabled();
......
......@@ -1074,6 +1074,43 @@ void main() {
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}));
test('debugToggleBrightness', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
...kAttachExpectations,
const FakeVmServiceRequest(
method: 'ext.flutter.brightnessOverride',
args: <String, Object>{
'isolateId': null,
},
jsonResponse: <String, Object>{
'value': 'Brightness.light'
},
),
const FakeVmServiceRequest(
method: 'ext.flutter.brightnessOverride',
args: <String, Object>{
'isolateId': null,
'value': 'Brightness.dark',
},
jsonResponse: <String, Object>{
'value': 'Brightness.dark'
},
),
]);
_setupMocks();
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
await residentWebRunner.debugToggleBrightness();
expect(testLogger.statusText,
contains('Changed brightness to Brightness.dark.'));
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}));
test('cleanup of resources is safe to call multiple times', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
...kAttachExpectations,
......
......@@ -88,6 +88,44 @@ void main() {
expect(response.type, 'Success');
});
test('ext.flutter.brightnessOverride can toggle window brightness', () async {
final IsolateRef isolate = (await vmService.getVM()).isolates.first;
final Response response = await vmService.callServiceExtension(
'ext.flutter.brightnessOverride',
isolateId: isolate.id,
);
expect(response.json['value'], 'Brightness.light');
final Response updateResponse = await vmService.callServiceExtension(
'ext.flutter.brightnessOverride',
isolateId: isolate.id,
args: <String, String>{
'value': 'Brightness.dark',
}
);
expect(updateResponse.json['value'], 'Brightness.dark');
// Change the brightness back to light
final Response verifyResponse = await vmService.callServiceExtension(
'ext.flutter.brightnessOverride',
isolateId: isolate.id,
args: <String, String>{
'value': 'Brightness.light',
}
);
expect(verifyResponse.json['value'], 'Brightness.light');
// Change with a bogus value
final Response bogusResponse = await vmService.callServiceExtension(
'ext.flutter.brightnessOverride',
isolateId: isolate.id,
args: <String, String>{
'value': 'dark', // Intentionally invalid value.
}
);
expect(bogusResponse.json['value'], 'Brightness.light');
});
// TODO(devoncarew): These tests fail on cirrus-ci windows.
}, skip: Platform.isWindows);
}
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