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'; ...@@ -8,7 +8,7 @@ import 'dart:async';
import 'dart:convert' show json; import 'dart:convert' show json;
import 'dart:developer' as developer; import 'dart:developer' as developer;
import 'dart:io' show exit; 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. // Before adding any more dart:ui imports, please read the README.
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
...@@ -143,14 +143,6 @@ abstract class BindingBase { ...@@ -143,14 +143,6 @@ abstract class BindingBase {
name: 'exit', name: 'exit',
callback: _exitApplication, callback: _exitApplication,
); );
registerServiceExtension(
name: 'saveCompilationTrace',
callback: (Map<String, String> parameters) async {
return <String, dynamic>{
'value': ui.saveCompilationTrace(),
};
},
);
} }
assert(() { assert(() {
...@@ -195,6 +187,33 @@ abstract class BindingBase { ...@@ -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; return true;
}()); }());
assert(() { assert(() {
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
// @dart = 2.8 // @dart = 2.8
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui show Brightness;
import 'assertions.dart'; import 'assertions.dart';
import 'platform.dart'; import 'platform.dart';
...@@ -27,7 +28,8 @@ bool debugAssertAllFoundationVarsUnset(String reason, { DebugPrintCallback debug ...@@ -27,7 +28,8 @@ bool debugAssertAllFoundationVarsUnset(String reason, { DebugPrintCallback debug
assert(() { assert(() {
if (debugPrint != debugPrintOverride || if (debugPrint != debugPrintOverride ||
debugDefaultTargetPlatformOverride != null || debugDefaultTargetPlatformOverride != null ||
debugDoublePrecision != null) debugDoublePrecision != null ||
debugBrightnessOverride != null)
throw FlutterError(reason); throw FlutterError(reason);
return true; return true;
}()); }());
...@@ -99,3 +101,12 @@ String debugFormatDouble(double value) { ...@@ -99,3 +101,12 @@ String debugFormatDouble(double value) {
} }
return value.toStringAsFixed(1); 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 ...@@ -1474,8 +1474,12 @@ class _MediaQueryFromWindowsState extends State<_MediaQueryFromWindow> with Widg
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window);
if (!kReleaseMode) {
data = data.copyWith(platformBrightness: debugBrightnessOverride);
}
return MediaQuery( return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window), data: data,
child: widget.child, child: widget.child,
); );
} }
......
...@@ -167,9 +167,8 @@ void main() { ...@@ -167,9 +167,8 @@ void main() {
// The following service extensions are disabled in web: // The following service extensions are disabled in web:
// 1. exit // 1. exit
// 2. saveCompilationTrace // 2. showPerformanceOverlay
// 3. showPerformanceOverlay const int disabledExtensions = kIsWeb ? 2 : 0;
const int disabledExtensions = kIsWeb ? 3 : 0;
// If you add a service extension... TEST IT! :-) // If you add a service extension... TEST IT! :-)
// ...then increment this number. // ...then increment this number.
expect(binding.extensions.length, 28 + widgetInspectorExtensionCount - disabledExtensions); expect(binding.extensions.length, 28 + widgetInspectorExtensionCount - disabledExtensions);
...@@ -711,15 +710,13 @@ void main() { ...@@ -711,15 +710,13 @@ void main() {
expect(binding.frameScheduled, isFalse); expect(binding.frameScheduled, isFalse);
}); });
test('Service extensions - saveCompilationTrace', () async { test('Service extensions - brightnessOverride', () async {
Map<String, dynamic> result; Map<String, dynamic> result;
result = await binding.testExtension('saveCompilationTrace', <String, String>{}); result = await binding.testExtension('brightnessOverride', <String, String>{});
final String trace = String.fromCharCodes((result['value'] as List<dynamic>).cast<int>()); final String brightnessValue = result['value'] as String;
expect(trace, contains('dart:core,Object,Object.\n'));
expect(trace, contains('package:test_api/test_api.dart,::,test\n')); expect(brightnessValue, 'Brightness.light');
expect(trace, contains('service_extensions_test.dart,::,main\n')); });
}, skip: isBrowser); // Compilation trace is Dart VM specific and not
// supported in browsers.
test('Service extensions - fastReassemble', () async { test('Service extensions - fastReassemble', () async {
Map<String, dynamic> result; Map<String, dynamic> result;
......
...@@ -8,7 +8,6 @@ import 'package:flutter/semantics.dart'; ...@@ -8,7 +8,6 @@ import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mockito/mockito.dart';
class StateMarker extends StatefulWidget { class StateMarker extends StatefulWidget {
const StateMarker({ Key key, this.child }) : super(key: key); const StateMarker({ Key key, this.child }) : super(key: key);
...@@ -837,4 +836,22 @@ void main() { ...@@ -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 { ...@@ -76,6 +76,13 @@ class CommandHelp {
'debugProfileWidgetBuilds', 'debugProfileWidgetBuilds',
); );
CommandHelpOption _b;
CommandHelpOption get b => _b ??= _makeOption(
'b',
'Toggle the platform brightness setting (dark and light mode).',
'debugBrightnessOverride',
);
CommandHelpOption _c; CommandHelpOption _c;
CommandHelpOption get c => _c ??= _makeOption( CommandHelpOption get c => _c ??= _makeOption(
'c', 'c',
......
...@@ -264,6 +264,30 @@ abstract class ResidentWebRunner extends ResidentRunner { ...@@ -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 @override
Future<void> stopEchoingDeviceLog() async { Future<void> stopEchoingDeviceLog() async {
// Do nothing for ResidentWebRunner // Do nothing for ResidentWebRunner
......
...@@ -425,6 +425,24 @@ class FlutterDevice { ...@@ -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 { Future<String> togglePlatform({ String from }) async {
final List<FlutterView> views = await vmService.getFlutterViews(); final List<FlutterView> views = await vmService.getFlutterViews();
final String to = nextPlatform(from, featureFlags); final String to = nextPlatform(from, featureFlags);
...@@ -973,6 +991,17 @@ abstract class ResidentRunner { ...@@ -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]. /// Take a screenshot on the provided [device].
/// ///
/// If the device has a connected vmservice, this method will attempt to hide /// If the device has a connected vmservice, this method will attempt to hide
...@@ -1221,6 +1250,7 @@ abstract class ResidentRunner { ...@@ -1221,6 +1250,7 @@ abstract class ResidentRunner {
commandHelp.s.print(); commandHelp.s.print();
} }
if (supportsServiceProtocol) { if (supportsServiceProtocol) {
commandHelp.b.print();
commandHelp.w.print(); commandHelp.w.print();
commandHelp.t.print(); commandHelp.t.print();
if (isRunningDebug) { if (isRunningDebug) {
...@@ -1362,6 +1392,9 @@ class TerminalHandler { ...@@ -1362,6 +1392,9 @@ class TerminalHandler {
return true; return true;
} }
return false; return false;
case 'b':
await residentRunner.debugToggleBrightness();
return true;
case 'c': case 'c':
residentRunner.clearScreen(); residentRunner.clearScreen();
return true; return true;
......
...@@ -708,6 +708,30 @@ extension FlutterVmService on vm_service.VmService { ...@@ -708,6 +708,30 @@ extension FlutterVmService on vm_service.VmService {
return 'unknown'; 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 /// Invoke a flutter extension method, if the flutter extension is not
/// available, returns null. /// available, returns null.
Future<Map<String, dynamic>> invokeFlutterExtensionRpcRaw( Future<Map<String, dynamic>> invokeFlutterExtensionRpcRaw(
...@@ -860,3 +884,19 @@ Future<String> sharedSkSlWriter(Device device, Map<String, Object> data, { ...@@ -860,3 +884,19 @@ Future<String> sharedSkSlWriter(Device device, Map<String, Object> data, {
globals.logger.printStatus('Wrote SkSL data to ${outputFile.path}.'); globals.logger.printStatus('Wrote SkSL data to ${outputFile.path}.');
return 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() { ...@@ -700,6 +700,7 @@ void main() {
commandHelp.c, commandHelp.c,
commandHelp.q, commandHelp.q,
commandHelp.s, commandHelp.s,
commandHelp.b,
commandHelp.w, commandHelp.w,
commandHelp.t, commandHelp.t,
commandHelp.L, commandHelp.L,
...@@ -1096,6 +1097,36 @@ void main() { ...@@ -1096,6 +1097,36 @@ void main() {
verify(mockFlutterDevice.toggleDebugPaintSizeEnabled()).called(1); 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 { testUsingContext('ResidentRunner debugToggleDebugCheckElevationsEnabled calls flutter device', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]); fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
await residentRunner.debugToggleDebugCheckElevationsEnabled(); await residentRunner.debugToggleDebugCheckElevationsEnabled();
......
...@@ -1074,6 +1074,43 @@ void main() { ...@@ -1074,6 +1074,43 @@ void main() {
expect(fakeVmServiceHost.hasRemainingExpectations, false); 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 { test('cleanup of resources is safe to call multiple times', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
...kAttachExpectations, ...kAttachExpectations,
......
...@@ -88,6 +88,44 @@ void main() { ...@@ -88,6 +88,44 @@ void main() {
expect(response.type, 'Success'); 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. // TODO(devoncarew): These tests fail on cirrus-ci windows.
}, skip: Platform.isWindows); }, 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