Skip to content

Commit e41c353

Browse files
authored
Support brightness override in a11y page (#9898)
* Update accessibility_controller.dart 1 * 1 Update overrides_pane.dart Update accessibility_controller.dart tests Update service_extensions.dart * resolve comments
1 parent 57f547b commit e41c353

6 files changed

Lines changed: 218 additions & 6 deletions

File tree

packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,31 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
44

5+
import 'dart:async';
6+
7+
import 'package:devtools_app_shared/service.dart';
58
import 'package:devtools_app_shared/utils.dart';
69
import 'package:flutter/foundation.dart';
710

11+
import '../../service/service_extensions.dart' as extensions;
812
import '../../shared/framework/screen.dart';
913
import '../../shared/framework/screen_controllers.dart';
14+
import '../../shared/globals.dart';
1015

1116
/// Modes for brightness override in the accessibility controls.
1217
enum BrightnessOverride {
13-
system('System Default'),
14-
light('Light Mode'),
15-
dark('Dark Mode');
18+
system('System Default', 'system'),
19+
light('Light Mode', 'Brightness.light'),
20+
dark('Dark Mode', 'Brightness.dark');
1621

17-
const BrightnessOverride(this.display);
22+
const BrightnessOverride(this.display, this.value);
1823

24+
/// The user-facing display label for this override option.
1925
final String display;
26+
27+
/// The raw value associated with this override option sent to or received
28+
/// from the VM service extension.
29+
final String value;
2030
}
2131

2232
/// Controller for the Accessibility screen.
@@ -26,6 +36,12 @@ class AccessibilityController extends DevToolsScreenController
2636
_initListeners();
2737
}
2838

39+
@override
40+
void init() {
41+
super.init();
42+
_initServiceExtensionStates();
43+
}
44+
2945
void _initListeners() {
3046
addAutoDisposeListener(brightness, _onBrightnessChanged);
3147
addAutoDisposeListener(textScale, _onTextScaleChanged);
@@ -34,9 +50,34 @@ class AccessibilityController extends DevToolsScreenController
3450
addAutoDisposeListener(highContrast, _onHighContrastChanged);
3551
}
3652

53+
void _initServiceExtensionStates() {
54+
final state = serviceConnection.serviceManager.serviceExtensionManager
55+
.getServiceExtensionState(extensions.brightnessMode.extension);
56+
57+
void updateFromDeviceState(ServiceExtensionState state) {
58+
final newBrightness = !state.enabled || state.value == null
59+
? BrightnessOverride.system
60+
: BrightnessOverride.values.firstWhere(
61+
(b) => b.value == state.value,
62+
orElse: () => BrightnessOverride.system,
63+
);
64+
brightness.value = newBrightness;
65+
}
66+
67+
updateFromDeviceState(state.value);
68+
addAutoDisposeListener(state, () => updateFromDeviceState(state.value));
69+
}
70+
3771
void _onBrightnessChanged() {
38-
// TODO(hannah-hyj): Implement VM service extension call for brightness override.
39-
// e.g. using 'ext.flutter.brightnessOverride'.
72+
final value = brightness.value;
73+
unawaited(
74+
serviceConnection.serviceManager.serviceExtensionManager
75+
.setServiceExtensionState(
76+
extensions.brightnessMode.extension,
77+
enabled: value != BrightnessOverride.system,
78+
value: value.value,
79+
),
80+
);
4081
}
4182

4283
void _onTextScaleChanged() {

packages/devtools_app/lib/src/service/service_extensions.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,16 @@ final togglePlatformMode = ServiceExtensionDescription<String>.from(
379379
tooltip: 'Override Target Platform',
380380
);
381381

382+
/// Service extension description for overriding brightness in accessibility controls.
383+
final brightnessMode = ServiceExtensionDescription<String>.from(
384+
extensions.brightnessMode,
385+
title: 'Override Brightness',
386+
iconData: Icons.brightness_6,
387+
gaScreenName: gac.accessibility,
388+
gaItem: gac.brightnessOverride,
389+
tooltip: 'Override Brightness',
390+
);
391+
382392
final disableClipLayers = ToggleableServiceExtensionDescription<bool>.from(
383393
extensions.disableClipLayers,
384394
title: 'Render Clip layers',

packages/devtools_app/lib/src/shared/analytics/constants.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ final vmTools = ScreenMetaData.vmTools.id;
4444
const console = 'console';
4545
final simple = ScreenMetaData.simple.id;
4646
final deeplink = ScreenMetaData.deepLinks.id;
47+
final accessibility = ScreenMetaData.accessibility.id;
4748

4849
// GA events not associated with a any screen e.g., hotReload, hotRestart, etc
4950
const devToolsMain = 'main';
@@ -86,6 +87,7 @@ const repaintRainbow = 'repaintRainbow';
8687
const repaintRainbowDocs = 'repaintRainbowDocs';
8788
const debugBanner = 'debugBanner';
8889
const togglePlatform = 'togglePlatform';
90+
const brightnessOverride = 'brightnessOverride';
8991
const highlightOversizedImages = 'highlightOversizedImages';
9092
const highlightOversizedImagesDocs = 'highlightOversizedImagesDocs';
9193
const selectWidgetMode = 'selectWidgetMode';
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright 2026 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
4+
5+
@TestOn('vm')
6+
library;
7+
8+
import 'package:devtools_app/devtools_app.dart';
9+
import 'package:devtools_app_shared/utils.dart';
10+
import 'package:devtools_test/devtools_test.dart';
11+
import 'package:flutter_test/flutter_test.dart';
12+
import 'package:mockito/mockito.dart';
13+
14+
void main() {
15+
group('AccessibilityController', () {
16+
late AccessibilityController controller;
17+
18+
setUp(() {
19+
final fakeServiceConnection = FakeServiceConnectionManager();
20+
when(
21+
fakeServiceConnection.serviceManager.connectedApp!.isFlutterWebAppNow,
22+
).thenReturn(false);
23+
when(
24+
fakeServiceConnection.serviceManager.connectedApp!.isProfileBuildNow,
25+
).thenReturn(false);
26+
27+
setGlobal(NotificationService, NotificationService());
28+
setGlobal(
29+
DevToolsEnvironmentParameters,
30+
ExternalDevToolsEnvironmentParameters(),
31+
);
32+
setGlobal(PreferencesController, PreferencesController());
33+
setGlobal(ServiceConnectionManager, fakeServiceConnection);
34+
35+
controller = AccessibilityController()..init();
36+
});
37+
38+
test('initial state', () {
39+
expect(controller.brightness.value, BrightnessOverride.system);
40+
});
41+
42+
test(
43+
'service extension state change updates controller brightness state',
44+
() {
45+
final fakeServiceExtensionManager =
46+
serviceConnection.serviceManager.serviceExtensionManager
47+
as FakeServiceExtensionManager;
48+
49+
expect(controller.brightness.value, BrightnessOverride.system);
50+
51+
// Simulate service extension state change from device to dark mode
52+
fakeServiceExtensionManager.fakeServiceExtensionStateChanged(
53+
brightnessMode.extension,
54+
'Brightness.dark',
55+
);
56+
expect(controller.brightness.value, BrightnessOverride.dark);
57+
58+
// Simulate service extension state change from device to light mode
59+
fakeServiceExtensionManager.fakeServiceExtensionStateChanged(
60+
brightnessMode.extension,
61+
'Brightness.light',
62+
);
63+
expect(controller.brightness.value, BrightnessOverride.light);
64+
65+
// Simulate service extension state change from device to system
66+
fakeServiceExtensionManager.fakeServiceExtensionStateChanged(
67+
brightnessMode.extension,
68+
'system',
69+
);
70+
expect(controller.brightness.value, BrightnessOverride.system);
71+
},
72+
);
73+
74+
test(
75+
'setting controller brightness updates service extension state',
76+
() async {
77+
final fakeServiceExtensionManager =
78+
serviceConnection.serviceManager.serviceExtensionManager
79+
as FakeServiceExtensionManager;
80+
81+
// Initial state
82+
expect(controller.brightness.value, BrightnessOverride.system);
83+
84+
// Set to dark mode
85+
controller.brightness.value = BrightnessOverride.dark;
86+
87+
// Wait for async operations to complete
88+
await Future<void>.delayed(Duration.zero);
89+
90+
final darkState = fakeServiceExtensionManager
91+
.getServiceExtensionState(brightnessMode.extension)
92+
.value;
93+
expect(darkState.value, equals('Brightness.dark'));
94+
expect(darkState.enabled, isTrue);
95+
96+
// Set to light mode
97+
controller.brightness.value = BrightnessOverride.light;
98+
await Future<void>.delayed(Duration.zero);
99+
100+
final lightState = fakeServiceExtensionManager
101+
.getServiceExtensionState(brightnessMode.extension)
102+
.value;
103+
expect(lightState.value, equals('Brightness.light'));
104+
expect(lightState.enabled, isTrue);
105+
106+
// Set to system
107+
controller.brightness.value = BrightnessOverride.system;
108+
await Future<void>.delayed(Duration.zero);
109+
110+
final systemState = fakeServiceExtensionManager
111+
.getServiceExtensionState(brightnessMode.extension)
112+
.value;
113+
expect(systemState.value, equals('system'));
114+
expect(systemState.enabled, isFalse);
115+
},
116+
);
117+
});
118+
}

packages/devtools_app_shared/lib/src/service/service_extensions.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,13 @@ final togglePlatformMode = ServiceExtension<String>(
201201
values: ['iOS', 'android', 'fuchsia', 'macOS', 'linux'],
202202
);
203203

204+
/// Override the platform brightness (light mode, dark mode, or system default).
205+
final brightnessMode = ServiceExtension<String>(
206+
extension:
207+
'$flutterExtensionPrefix${FoundationServiceExtensions.brightnessOverride.name}',
208+
values: ['system', 'Brightness.light', 'Brightness.dark'],
209+
);
210+
204211
/// Toggle whether interacting with the device selects widgets or triggers
205212
/// normal interactions.
206213
final toggleSelectWidgetMode = ToggleableServiceExtension<bool>(
@@ -259,6 +266,7 @@ final _extensionDescriptions = <ServiceExtension<Object>>[
259266
toggleSelectWidgetMode,
260267
countWidgetBuilds,
261268
profilePlatformChannels,
269+
brightnessMode,
262270
];
263271

264272
/// Service extensions that are not safe to call unless a frame has already
@@ -281,6 +289,7 @@ final _unsafeBeforeFirstFrameFlutterExtensions = Set.of(
281289
enableOnDeviceInspector,
282290
togglePlatformMode,
283291
slowAnimations,
292+
brightnessMode,
284293
].map((extension) => extension.extension),
285294
);
286295

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2026 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
4+
5+
import 'package:devtools_app_shared/service_extensions.dart';
6+
import 'package:test/test.dart';
7+
8+
void main() {
9+
group('ServiceExtensions', () {
10+
test('brightnessMode is properly configured', () {
11+
expect(
12+
brightnessMode.extension,
13+
equals('ext.flutter.brightnessOverride'),
14+
);
15+
expect(
16+
brightnessMode.values,
17+
equals(['system', 'Brightness.light', 'Brightness.dark']),
18+
);
19+
expect(
20+
serviceExtensionsAllowlist[brightnessMode.extension],
21+
equals(brightnessMode),
22+
);
23+
});
24+
25+
test('brightnessMode is in unsafe before first frame set', () {
26+
expect(
27+
isUnsafeBeforeFirstFlutterFrame('ext.flutter.brightnessOverride'),
28+
isTrue,
29+
);
30+
});
31+
});
32+
}

0 commit comments

Comments
 (0)