From 1faa3145b1961cc4eb1831f0a54bbb03289b8419 Mon Sep 17 00:00:00 2001 From: hangyu Date: Wed, 15 Jul 2026 11:38:44 -0700 Subject: [PATCH 1/3] Update accessibility_controller.dart 1 --- .../accessibility_controller.dart | 58 ++++++++++++++++++- .../lib/src/service/service_extensions.dart | 15 +++++ .../lib/src/shared/analytics/constants.dart | 2 + .../lib/src/service/service_extensions.dart | 9 +++ 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart index 887ddf5b9fb..31cca2890fa 100644 --- a/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart +++ b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart @@ -2,11 +2,16 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. +import 'dart:async'; + +import 'package:devtools_app_shared/service.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/foundation.dart'; +import '../../service/service_extensions.dart' as extensions; import '../../shared/framework/screen.dart'; import '../../shared/framework/screen_controllers.dart'; +import '../../shared/globals.dart'; /// Modes for brightness override in the accessibility controls. enum BrightnessOverride { @@ -26,6 +31,12 @@ class AccessibilityController extends DevToolsScreenController _initListeners(); } + @override + void init() { + super.init(); + _initServiceExtensionStates(); + } + void _initListeners() { addAutoDisposeListener(brightness, _onBrightnessChanged); addAutoDisposeListener(textScale, _onTextScaleChanged); @@ -34,9 +45,52 @@ class AccessibilityController extends DevToolsScreenController addAutoDisposeListener(highContrast, _onHighContrastChanged); } + void _initServiceExtensionStates() { + final state = serviceConnection.serviceManager.serviceExtensionManager + .getServiceExtensionState(extensions.brightnessMode.extension); + + void updateFromDeviceState(ServiceExtensionState s) { + final newBrightness = !s.enabled || s.value == null + ? BrightnessOverride.system + : switch (s.value) { + 'Brightness.light' => BrightnessOverride.light, + 'Brightness.dark' => BrightnessOverride.dark, + _ => BrightnessOverride.system, + }; + if (brightness.value != newBrightness) { + brightness.value = newBrightness; + } + } + + updateFromDeviceState(state.value); + addAutoDisposeListener( + state, + () => updateFromDeviceState(state.value), + ); + } + + + void _onBrightnessChanged() { - // TODO(hannah-hyj): Implement VM service extension call for brightness override. - // e.g. using 'ext.flutter.brightnessOverride'. + final value = brightness.value; + // Values expected by Flutter framework's 'ext.flutter.brightnessOverride': + // - 'Brightness.light': forces light mode + // - 'Brightness.dark': forces dark mode + // - '': any value other than 'Brightness.light' or 'Brightness.dark' clears + // the override and resets to system default. + final paramValue = switch (value) { + BrightnessOverride.light => 'Brightness.light', + BrightnessOverride.dark => 'Brightness.dark', + BrightnessOverride.system => '', + }; + unawaited( + serviceConnection.serviceManager.serviceExtensionManager + .setServiceExtensionState( + extensions.brightnessMode.extension, + enabled: value != BrightnessOverride.system, + value: paramValue, + ), + ); } void _onTextScaleChanged() { diff --git a/packages/devtools_app/lib/src/service/service_extensions.dart b/packages/devtools_app/lib/src/service/service_extensions.dart index 7e20efaeb01..3c6de2ca465 100644 --- a/packages/devtools_app/lib/src/service/service_extensions.dart +++ b/packages/devtools_app/lib/src/service/service_extensions.dart @@ -379,6 +379,21 @@ final togglePlatformMode = ServiceExtensionDescription.from( tooltip: 'Override Target Platform', ); +/// Service extension description for overriding brightness in accessibility controls. +final brightnessMode = ServiceExtensionDescription.from( + extensions.brightnessMode, + title: 'Override brightness', + iconData: Icons.brightness_6, + displayValues: [ + 'System Default', + 'Light Mode', + 'Dark Mode', + ], + gaScreenName: gac.accessibility, + gaItem: gac.brightnessOverride, + tooltip: 'Override Brightness', +); + final disableClipLayers = ToggleableServiceExtensionDescription.from( extensions.disableClipLayers, title: 'Render Clip layers', diff --git a/packages/devtools_app/lib/src/shared/analytics/constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants.dart index 3d293268840..c56654fc5c2 100644 --- a/packages/devtools_app/lib/src/shared/analytics/constants.dart +++ b/packages/devtools_app/lib/src/shared/analytics/constants.dart @@ -44,6 +44,7 @@ final vmTools = ScreenMetaData.vmTools.id; const console = 'console'; final simple = ScreenMetaData.simple.id; final deeplink = ScreenMetaData.deepLinks.id; +final accessibility = ScreenMetaData.accessibility.id; // GA events not associated with a any screen e.g., hotReload, hotRestart, etc const devToolsMain = 'main'; @@ -86,6 +87,7 @@ const repaintRainbow = 'repaintRainbow'; const repaintRainbowDocs = 'repaintRainbowDocs'; const debugBanner = 'debugBanner'; const togglePlatform = 'togglePlatform'; +const brightnessOverride = 'brightnessOverride'; const highlightOversizedImages = 'highlightOversizedImages'; const highlightOversizedImagesDocs = 'highlightOversizedImagesDocs'; const selectWidgetMode = 'selectWidgetMode'; diff --git a/packages/devtools_app_shared/lib/src/service/service_extensions.dart b/packages/devtools_app_shared/lib/src/service/service_extensions.dart index cada4b02b97..608c7c8090d 100644 --- a/packages/devtools_app_shared/lib/src/service/service_extensions.dart +++ b/packages/devtools_app_shared/lib/src/service/service_extensions.dart @@ -201,6 +201,13 @@ final togglePlatformMode = ServiceExtension( values: ['iOS', 'android', 'fuchsia', 'macOS', 'linux'], ); +/// Override the platform brightness (light mode, dark mode, or system default). +final brightnessMode = ServiceExtension( + extension: + '$flutterExtensionPrefix${FoundationServiceExtensions.brightnessOverride.name}', + values: ['system', 'Brightness.light', 'Brightness.dark'], +); + /// Toggle whether interacting with the device selects widgets or triggers /// normal interactions. final toggleSelectWidgetMode = ToggleableServiceExtension( @@ -259,6 +266,7 @@ final _extensionDescriptions = >[ toggleSelectWidgetMode, countWidgetBuilds, profilePlatformChannels, + brightnessMode, ]; /// Service extensions that are not safe to call unless a frame has already @@ -281,6 +289,7 @@ final _unsafeBeforeFirstFrameFlutterExtensions = Set.of( enableOnDeviceInspector, togglePlatformMode, slowAnimations, + brightnessMode, ].map((extension) => extension.extension), ); From cd275cbd8961b0a1a2eda95e7646637db0e2c40f Mon Sep 17 00:00:00 2001 From: hangyu Date: Thu, 16 Jul 2026 13:33:56 -0700 Subject: [PATCH 2/3] 1 Update overrides_pane.dart Update accessibility_controller.dart tests Update service_extensions.dart --- .../accessibility_controller.dart | 2 -- .../lib/src/service/service_extensions.dart | 5 --- .../accessibility_screen_test.dart | 31 ++++++++++++++++++ .../test/service/service_extensions_test.dart | 32 +++++++++++++++++++ 4 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 packages/devtools_app_shared/test/service/service_extensions_test.dart diff --git a/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart index 31cca2890fa..d71da3f7b5f 100644 --- a/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart +++ b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart @@ -69,8 +69,6 @@ class AccessibilityController extends DevToolsScreenController ); } - - void _onBrightnessChanged() { final value = brightness.value; // Values expected by Flutter framework's 'ext.flutter.brightnessOverride': diff --git a/packages/devtools_app/lib/src/service/service_extensions.dart b/packages/devtools_app/lib/src/service/service_extensions.dart index 3c6de2ca465..0a6bf4d6f86 100644 --- a/packages/devtools_app/lib/src/service/service_extensions.dart +++ b/packages/devtools_app/lib/src/service/service_extensions.dart @@ -384,11 +384,6 @@ final brightnessMode = ServiceExtensionDescription.from( extensions.brightnessMode, title: 'Override brightness', iconData: Icons.brightness_6, - displayValues: [ - 'System Default', - 'Light Mode', - 'Dark Mode', - ], gaScreenName: gac.accessibility, gaItem: gac.brightnessOverride, tooltip: 'Override Brightness', diff --git a/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart b/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart index ba5e53e637b..ff2ddcda556 100644 --- a/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart +++ b/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart @@ -181,5 +181,36 @@ void main() { expect(controller.highContrast.value, isTrue); }, ); + + testWidgetsWithWindowSize( + 'service extension state change updates controller brightness state', + windowSize, + (WidgetTester tester) async { + final fakeServiceExtensionManager = + serviceConnection.serviceManager.serviceExtensionManager + as FakeServiceExtensionManager; + + await pumpAccessibilityScreen(tester); + await tester.pumpAndSettle(); + + expect(controller.brightness.value, BrightnessOverride.system); + + // Simulate service extension state change from device to dark mode + fakeServiceExtensionManager.fakeServiceExtensionStateChanged( + brightnessMode.extension, + 'Brightness.dark', + ); + await tester.pumpAndSettle(); + expect(controller.brightness.value, BrightnessOverride.dark); + + // Simulate service extension state change from device to light mode + fakeServiceExtensionManager.fakeServiceExtensionStateChanged( + brightnessMode.extension, + 'Brightness.light', + ); + await tester.pumpAndSettle(); + expect(controller.brightness.value, BrightnessOverride.light); + }, + ); }); } diff --git a/packages/devtools_app_shared/test/service/service_extensions_test.dart b/packages/devtools_app_shared/test/service/service_extensions_test.dart new file mode 100644 index 00000000000..2ee39d2ef02 --- /dev/null +++ b/packages/devtools_app_shared/test/service/service_extensions_test.dart @@ -0,0 +1,32 @@ +// Copyright 2026 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'package:devtools_app_shared/service_extensions.dart'; +import 'package:test/test.dart'; + +void main() { + group('ServiceExtensions', () { + test('brightnessMode is properly configured', () { + expect( + brightnessMode.extension, + equals('ext.flutter.brightnessOverride'), + ); + expect( + brightnessMode.values, + equals(['system', 'Brightness.light', 'Brightness.dark']), + ); + expect( + serviceExtensionsAllowlist[brightnessMode.extension], + equals(brightnessMode), + ); + }); + + test('brightnessMode is in unsafe before first frame set', () { + expect( + isUnsafeBeforeFirstFlutterFrame('ext.flutter.brightnessOverride'), + isTrue, + ); + }); + }); +} From c992950b767af06610d78ee909e5df5f9267f8d1 Mon Sep 17 00:00:00 2001 From: hangyu Date: Fri, 17 Jul 2026 11:22:04 -0700 Subject: [PATCH 3/3] resolve comments --- .../accessibility_controller.dart | 53 ++++---- .../lib/src/service/service_extensions.dart | 2 +- .../accessibility_controller_test.dart | 118 ++++++++++++++++++ .../accessibility_screen_test.dart | 31 ----- 4 files changed, 140 insertions(+), 64 deletions(-) create mode 100644 packages/devtools_app/test/screens/accessibility/accessibility_controller_test.dart diff --git a/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart index d71da3f7b5f..1fd71a307f6 100644 --- a/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart +++ b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart @@ -15,13 +15,18 @@ import '../../shared/globals.dart'; /// Modes for brightness override in the accessibility controls. enum BrightnessOverride { - system('System Default'), - light('Light Mode'), - dark('Dark Mode'); + system('System Default', 'system'), + light('Light Mode', 'Brightness.light'), + dark('Dark Mode', 'Brightness.dark'); - const BrightnessOverride(this.display); + const BrightnessOverride(this.display, this.value); + /// The user-facing display label for this override option. final String display; + + /// The raw value associated with this override option sent to or received + /// from the VM service extension. + final String value; } /// Controller for the Accessibility screen. @@ -49,45 +54,29 @@ class AccessibilityController extends DevToolsScreenController final state = serviceConnection.serviceManager.serviceExtensionManager .getServiceExtensionState(extensions.brightnessMode.extension); - void updateFromDeviceState(ServiceExtensionState s) { - final newBrightness = !s.enabled || s.value == null + void updateFromDeviceState(ServiceExtensionState state) { + final newBrightness = !state.enabled || state.value == null ? BrightnessOverride.system - : switch (s.value) { - 'Brightness.light' => BrightnessOverride.light, - 'Brightness.dark' => BrightnessOverride.dark, - _ => BrightnessOverride.system, - }; - if (brightness.value != newBrightness) { - brightness.value = newBrightness; - } + : BrightnessOverride.values.firstWhere( + (b) => b.value == state.value, + orElse: () => BrightnessOverride.system, + ); + brightness.value = newBrightness; } updateFromDeviceState(state.value); - addAutoDisposeListener( - state, - () => updateFromDeviceState(state.value), - ); + addAutoDisposeListener(state, () => updateFromDeviceState(state.value)); } void _onBrightnessChanged() { final value = brightness.value; - // Values expected by Flutter framework's 'ext.flutter.brightnessOverride': - // - 'Brightness.light': forces light mode - // - 'Brightness.dark': forces dark mode - // - '': any value other than 'Brightness.light' or 'Brightness.dark' clears - // the override and resets to system default. - final paramValue = switch (value) { - BrightnessOverride.light => 'Brightness.light', - BrightnessOverride.dark => 'Brightness.dark', - BrightnessOverride.system => '', - }; unawaited( serviceConnection.serviceManager.serviceExtensionManager .setServiceExtensionState( - extensions.brightnessMode.extension, - enabled: value != BrightnessOverride.system, - value: paramValue, - ), + extensions.brightnessMode.extension, + enabled: value != BrightnessOverride.system, + value: value.value, + ), ); } diff --git a/packages/devtools_app/lib/src/service/service_extensions.dart b/packages/devtools_app/lib/src/service/service_extensions.dart index 0a6bf4d6f86..b2bf785eb94 100644 --- a/packages/devtools_app/lib/src/service/service_extensions.dart +++ b/packages/devtools_app/lib/src/service/service_extensions.dart @@ -382,7 +382,7 @@ final togglePlatformMode = ServiceExtensionDescription.from( /// Service extension description for overriding brightness in accessibility controls. final brightnessMode = ServiceExtensionDescription.from( extensions.brightnessMode, - title: 'Override brightness', + title: 'Override Brightness', iconData: Icons.brightness_6, gaScreenName: gac.accessibility, gaItem: gac.brightnessOverride, diff --git a/packages/devtools_app/test/screens/accessibility/accessibility_controller_test.dart b/packages/devtools_app/test/screens/accessibility/accessibility_controller_test.dart new file mode 100644 index 00000000000..1363f9d330c --- /dev/null +++ b/packages/devtools_app/test/screens/accessibility/accessibility_controller_test.dart @@ -0,0 +1,118 @@ +// Copyright 2026 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +@TestOn('vm') +library; + +import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app_shared/utils.dart'; +import 'package:devtools_test/devtools_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +void main() { + group('AccessibilityController', () { + late AccessibilityController controller; + + setUp(() { + final fakeServiceConnection = FakeServiceConnectionManager(); + when( + fakeServiceConnection.serviceManager.connectedApp!.isFlutterWebAppNow, + ).thenReturn(false); + when( + fakeServiceConnection.serviceManager.connectedApp!.isProfileBuildNow, + ).thenReturn(false); + + setGlobal(NotificationService, NotificationService()); + setGlobal( + DevToolsEnvironmentParameters, + ExternalDevToolsEnvironmentParameters(), + ); + setGlobal(PreferencesController, PreferencesController()); + setGlobal(ServiceConnectionManager, fakeServiceConnection); + + controller = AccessibilityController()..init(); + }); + + test('initial state', () { + expect(controller.brightness.value, BrightnessOverride.system); + }); + + test( + 'service extension state change updates controller brightness state', + () { + final fakeServiceExtensionManager = + serviceConnection.serviceManager.serviceExtensionManager + as FakeServiceExtensionManager; + + expect(controller.brightness.value, BrightnessOverride.system); + + // Simulate service extension state change from device to dark mode + fakeServiceExtensionManager.fakeServiceExtensionStateChanged( + brightnessMode.extension, + 'Brightness.dark', + ); + expect(controller.brightness.value, BrightnessOverride.dark); + + // Simulate service extension state change from device to light mode + fakeServiceExtensionManager.fakeServiceExtensionStateChanged( + brightnessMode.extension, + 'Brightness.light', + ); + expect(controller.brightness.value, BrightnessOverride.light); + + // Simulate service extension state change from device to system + fakeServiceExtensionManager.fakeServiceExtensionStateChanged( + brightnessMode.extension, + 'system', + ); + expect(controller.brightness.value, BrightnessOverride.system); + }, + ); + + test( + 'setting controller brightness updates service extension state', + () async { + final fakeServiceExtensionManager = + serviceConnection.serviceManager.serviceExtensionManager + as FakeServiceExtensionManager; + + // Initial state + expect(controller.brightness.value, BrightnessOverride.system); + + // Set to dark mode + controller.brightness.value = BrightnessOverride.dark; + + // Wait for async operations to complete + await Future.delayed(Duration.zero); + + final darkState = fakeServiceExtensionManager + .getServiceExtensionState(brightnessMode.extension) + .value; + expect(darkState.value, equals('Brightness.dark')); + expect(darkState.enabled, isTrue); + + // Set to light mode + controller.brightness.value = BrightnessOverride.light; + await Future.delayed(Duration.zero); + + final lightState = fakeServiceExtensionManager + .getServiceExtensionState(brightnessMode.extension) + .value; + expect(lightState.value, equals('Brightness.light')); + expect(lightState.enabled, isTrue); + + // Set to system + controller.brightness.value = BrightnessOverride.system; + await Future.delayed(Duration.zero); + + final systemState = fakeServiceExtensionManager + .getServiceExtensionState(brightnessMode.extension) + .value; + expect(systemState.value, equals('system')); + expect(systemState.enabled, isFalse); + }, + ); + }); +} diff --git a/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart b/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart index ff2ddcda556..ba5e53e637b 100644 --- a/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart +++ b/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart @@ -181,36 +181,5 @@ void main() { expect(controller.highContrast.value, isTrue); }, ); - - testWidgetsWithWindowSize( - 'service extension state change updates controller brightness state', - windowSize, - (WidgetTester tester) async { - final fakeServiceExtensionManager = - serviceConnection.serviceManager.serviceExtensionManager - as FakeServiceExtensionManager; - - await pumpAccessibilityScreen(tester); - await tester.pumpAndSettle(); - - expect(controller.brightness.value, BrightnessOverride.system); - - // Simulate service extension state change from device to dark mode - fakeServiceExtensionManager.fakeServiceExtensionStateChanged( - brightnessMode.extension, - 'Brightness.dark', - ); - await tester.pumpAndSettle(); - expect(controller.brightness.value, BrightnessOverride.dark); - - // Simulate service extension state change from device to light mode - fakeServiceExtensionManager.fakeServiceExtensionStateChanged( - brightnessMode.extension, - 'Brightness.light', - ); - await tester.pumpAndSettle(); - expect(controller.brightness.value, BrightnessOverride.light); - }, - ); }); }