diff --git a/DEPS b/DEPS index 85423d118126d..8dd1380e3c7b7 100644 --- a/DEPS +++ b/DEPS @@ -35,7 +35,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/master/DEPS. # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': '9a1827339c8e4e55a9b214d033aeba7d30daa28f', + 'dart_revision': 'aa7d19d185583b221093ad7385cf91fca3e8779c', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py diff --git a/ci/licenses_golden/licenses_third_party b/ci/licenses_golden/licenses_third_party index db3e58410dc65..3f26332fc7f6e 100644 --- a/ci/licenses_golden/licenses_third_party +++ b/ci/licenses_golden/licenses_third_party @@ -1,4 +1,4 @@ -Signature: 762dc8deadc282521ea04cfc1f0973e9 +Signature: 988f1584a445473114bc2516f591f26b UNUSED LICENSES: diff --git a/lib/web_ui/lib/src/engine/navigation/history.dart b/lib/web_ui/lib/src/engine/navigation/history.dart index c5eec94e78f02..9c6de07e95edf 100644 --- a/lib/web_ui/lib/src/engine/navigation/history.dart +++ b/lib/web_ui/lib/src/engine/navigation/history.dart @@ -11,6 +11,20 @@ import '../services/message_codec.dart'; import '../services/message_codecs.dart'; import 'url_strategy.dart'; +/// Infers the history mode from the existing browser history state, then +/// creates the appropriate instance of [BrowserHistory] for it. +/// +/// If it can't infer, it creates a [MultiEntriesBrowserHistory] by default. +BrowserHistory createHistoryForExistingState(UrlStrategy? urlStrategy) { + if (urlStrategy != null) { + final Object? state = urlStrategy.getState(); + if (SingleEntryBrowserHistory._isOriginEntry(state) || SingleEntryBrowserHistory._isFlutterEntry(state)) { + return SingleEntryBrowserHistory(urlStrategy: urlStrategy); + } + } + return MultiEntriesBrowserHistory(urlStrategy: urlStrategy); +} + /// An abstract class that provides the API for [EngineWindow] to delegate its /// navigating events. /// @@ -263,14 +277,14 @@ class SingleEntryBrowserHistory extends BrowserHistory { /// The origin entry is the history entry that the Flutter app landed on. It's /// created by the browser when the user navigates to the url of the app. - bool _isOriginEntry(Object? state) { + static bool _isOriginEntry(Object? state) { return state is Map && state[_kOriginTag] == true; } /// The flutter entry is a history entry that we maintain on top of the origin /// entry. It allows us to catch popstate events when the user hits the back /// button. - bool _isFlutterEntry(Object? state) { + static bool _isFlutterEntry(Object? state) { return state is Map && state[_kFlutterTag] == true; } diff --git a/lib/web_ui/lib/src/engine/window.dart b/lib/web_ui/lib/src/engine/window.dart index 0e5d6f9aa207a..353e9a26e0d0e 100644 --- a/lib/web_ui/lib/src/engine/window.dart +++ b/lib/web_ui/lib/src/engine/window.dart @@ -13,7 +13,6 @@ import 'package:js/js.dart'; import 'package:meta/meta.dart'; import 'package:ui/ui.dart' as ui; -import '../engine.dart' show registerHotRestartListener; import 'browser_detection.dart'; import 'navigation/history.dart'; import 'navigation/js_url_strategy.dart'; @@ -49,12 +48,8 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow { engineDispatcher.windows[_windowId] = this; engineDispatcher.windowConfigurations[_windowId] = const ui.ViewConfiguration(); if (_isUrlStrategySet) { - _browserHistory = - MultiEntriesBrowserHistory(urlStrategy: _customUrlStrategy); + _browserHistory = createHistoryForExistingState(_customUrlStrategy); } - registerHotRestartListener(() { - window.resetHistory(); - }); } final Object _windowId; @@ -66,7 +61,7 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow { /// button, etc. BrowserHistory get browserHistory { return _browserHistory ??= - MultiEntriesBrowserHistory(urlStrategy: _urlStrategyForInitialization); + createHistoryForExistingState(_urlStrategyForInitialization); } UrlStrategy? get _urlStrategyForInitialization { @@ -81,32 +76,50 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow { _browserHistory; // Must be either SingleEntryBrowserHistory or MultiEntriesBrowserHistory. Future _useSingleEntryBrowserHistory() async { + // Recreate the browser history mode that's appropriate for the existing + // history state. + // + // If it happens to be a single-entry one, then there's nothing further to do. + // + // But if it's a multi-entry one, it will be torn down below and replaced + // with a single-entry history. + // + // See: https://github.com/flutter/flutter/issues/79241 + _browserHistory ??= + createHistoryForExistingState(_urlStrategyForInitialization); + if (_browserHistory is SingleEntryBrowserHistory) { return; } - final UrlStrategy? strategy; - if (_browserHistory == null) { - strategy = _urlStrategyForInitialization; - } else { - strategy = _browserHistory?.urlStrategy; - await _browserHistory?.tearDown(); - } + // At this point, we know that `_browserHistory` is a non-null + // `MultiEntriesBrowserHistory` instance. + final UrlStrategy? strategy = _browserHistory?.urlStrategy; + await _browserHistory?.tearDown(); _browserHistory = SingleEntryBrowserHistory(urlStrategy: strategy); } Future _useMultiEntryBrowserHistory() async { + // Recreate the browser history mode that's appropriate for the existing + // history state. + // + // If it happens to be a multi-entry one, then there's nothing further to do. + // + // But if it's a single-entry one, it will be torn down below and replaced + // with a multi-entry history. + // + // See: https://github.com/flutter/flutter/issues/79241 + _browserHistory ??= + createHistoryForExistingState(_urlStrategyForInitialization); + if (_browserHistory is MultiEntriesBrowserHistory) { return; } - final UrlStrategy? strategy; - if (_browserHistory == null) { - strategy = _urlStrategyForInitialization; - } else { - strategy = _browserHistory?.urlStrategy; - await _browserHistory?.tearDown(); - } + // At this point, we know that `_browserHistory` is a non-null + // `SingleEntryBrowserHistory` instance. + final UrlStrategy? strategy = _browserHistory?.urlStrategy; + await _browserHistory?.tearDown(); _browserHistory = MultiEntriesBrowserHistory(urlStrategy: strategy); } diff --git a/lib/web_ui/test/engine/history_test.dart b/lib/web_ui/test/engine/history_test.dart index 08b2aed6ad856..7f87324b4091d 100644 --- a/lib/web_ui/test/engine/history_test.dart +++ b/lib/web_ui/test/engine/history_test.dart @@ -39,6 +39,50 @@ void main() { } void testMain() { + test('createHistoryForExistingState', () { + TestUrlStrategy strategy; + BrowserHistory history; + + // No url strategy. + history = createHistoryForExistingState(null); + expect(history, isA()); + expect(history.urlStrategy, isNull); + + // Random history state. + strategy = TestUrlStrategy.fromEntry( + const TestHistoryEntry({'foo': 123}, null, '/'), + ); + history = createHistoryForExistingState(strategy); + expect(history, isA()); + expect(history.urlStrategy, strategy); + + // Multi-entry history state. + final Map state = { + 'serialCount': 1, + 'state': {'foo': 123}, + }; + strategy = TestUrlStrategy.fromEntry(TestHistoryEntry(state, null, '/')); + history = createHistoryForExistingState(strategy); + expect(history, isA()); + expect(history.urlStrategy, strategy); + + // Single-entry history "origin" state. + strategy = TestUrlStrategy.fromEntry( + const TestHistoryEntry({'origin': true}, null, '/'), + ); + history = createHistoryForExistingState(strategy); + expect(history, isA()); + expect(history.urlStrategy, strategy); + + // Single-entry history "flutter" state. + strategy = TestUrlStrategy.fromEntry( + const TestHistoryEntry({'flutter': true}, null, '/'), + ); + history = createHistoryForExistingState(strategy); + expect(history, isA()); + expect(history.urlStrategy, strategy); + }); + group('$SingleEntryBrowserHistory', () { final PlatformMessagesSpy spy = PlatformMessagesSpy(); diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java index 13dacbee2c9ef..f5ab23c18a412 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java @@ -374,15 +374,17 @@ private void setSystemChromeSystemUIOverlayStyle( // If transparent, SDK 29 and higher may apply a translucent scrim behind the bar to ensure // proper contrast. This can be overridden with // SystemChromeStyle.systemStatusBarContrastEnforced. - if (systemChromeStyle.statusBarIconBrightness != null && Build.VERSION.SDK_INT >= 23) { - switch (systemChromeStyle.statusBarIconBrightness) { - case DARK: - // View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - flags |= 0x2000; - break; - case LIGHT: - flags &= ~0x2000; - break; + if (Build.VERSION.SDK_INT >= 23) { + if (systemChromeStyle.statusBarIconBrightness != null) { + switch (systemChromeStyle.statusBarIconBrightness) { + case DARK: + // View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + flags |= 0x2000; + break; + case LIGHT: + flags &= ~0x2000; + break; + } } if (systemChromeStyle.statusBarColor != null) { @@ -403,16 +405,17 @@ private void setSystemChromeSystemUIOverlayStyle( // If transparent, SDK 29 and higher may apply a translucent scrim behind 2/3 button navigation // bars to ensure proper contrast. This can be overridden with // SystemChromeStyle.systemNavigationBarContrastEnforced. - if (systemChromeStyle.systemNavigationBarIconBrightness != null - && Build.VERSION.SDK_INT >= 26) { - switch (systemChromeStyle.systemNavigationBarIconBrightness) { - case DARK: - // View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR - flags |= 0x10; - break; - case LIGHT: - flags &= ~0x10; - break; + if (Build.VERSION.SDK_INT >= 26) { + if (systemChromeStyle.systemNavigationBarIconBrightness != null) { + switch (systemChromeStyle.systemNavigationBarIconBrightness) { + case DARK: + // View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + flags |= 0x10; + break; + case LIGHT: + flags &= ~0x10; + break; + } } if (systemChromeStyle.systemNavigationBarColor != null) { diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java index e67769cef904e..fea6db961e248 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java @@ -162,15 +162,41 @@ public void setNavigationBarDividerColor() { when(fakeActivity.getWindow()).thenReturn(fakeWindow); PlatformChannel fakePlatformChannel = mock(PlatformChannel.class); PlatformPlugin platformPlugin = new PlatformPlugin(fakeActivity, fakePlatformChannel); + // Default style test SystemChromeStyle style = - new SystemChromeStyle(0XFF000000, null, true, 0XFFC70039, null, 0XFF006DB3, true); + new SystemChromeStyle( + 0XFF000000, // statusBarColor + null, // statusBarIconBrightness + true, // systemStatusBarContrastEnforced + 0XFFC70039, // systemNavigationBarColor + null, // systemNavigationBarIconBrightness + 0XFF006DB3, // systemNavigationBarDividerColor + true); // systemNavigationBarContrastEnforced if (Build.VERSION.SDK_INT >= 28) { platformPlugin.mPlatformMessageHandler.setSystemUiOverlayStyle(style); + assertEquals(0XFF000000, fakeActivity.getWindow().getStatusBarColor()); + assertEquals(0XFFC70039, fakeActivity.getWindow().getNavigationBarColor()); assertEquals(0XFF006DB3, fakeActivity.getWindow().getNavigationBarDividerColor()); + + // Regression test for https://github.com/flutter/flutter/issues/88431 + // A null brightness should not affect changing color settings. + style = + new SystemChromeStyle( + 0XFF006DB3, // statusBarColor + null, // statusBarIconBrightness + true, // systemStatusBarContrastEnforced + 0XFF000000, // systemNavigationBarColor + null, // systemNavigationBarIconBrightness + 0XFF006DB3, // systemNavigationBarDividerColor + true); // systemNavigationBarContrastEnforced + + platformPlugin.mPlatformMessageHandler.setSystemUiOverlayStyle(style); + assertEquals(0XFFC70039, fakeActivity.getWindow().getStatusBarColor()); assertEquals(0XFF000000, fakeActivity.getWindow().getNavigationBarColor()); + assertEquals(0XFF006DB3, fakeActivity.getWindow().getNavigationBarDividerColor()); } } diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm index 76e5145443b0a..57c8c67c048b7 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm @@ -54,7 +54,9 @@ CGPoint ConvertPointToGlobal(SemanticsObject* reference, CGPoint local_point) { // `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in // the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to // convert. - CGFloat scale = [[[reference bridge]->view() window] screen].scale; + UIScreen* screen = [[[reference bridge]->view() window] screen]; + // Screen can be nil if the FlutterView is covered by another native view. + CGFloat scale = screen == nil ? [UIScreen mainScreen].scale : screen.scale; auto result = CGPointMake(point.x() / scale, point.y() / scale); return [[reference bridge]->view() convertPoint:result toView:nil]; } @@ -80,7 +82,9 @@ CGRect ConvertRectToGlobal(SemanticsObject* reference, CGRect local_rect) { // `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in // the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to // convert. - CGFloat scale = [[[reference bridge]->view() window] screen].scale; + UIScreen* screen = [[[reference bridge]->view() window] screen]; + // Screen can be nil if the FlutterView is covered by another native view. + CGFloat scale = screen == nil ? [UIScreen mainScreen].scale : screen.scale; auto result = CGRectMake(rect.x() / scale, rect.y() / scale, rect.width() / scale, rect.height() / scale); return UIAccessibilityConvertFrameToScreenCoordinates(result, [reference bridge]->view()); diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm index 77e0edfc73e46..74d478bc2fa4a 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm @@ -56,6 +56,36 @@ void AccessibilityObjectDidLoseFocus(int32_t id) override {} UIView* view_; UIWindow* window_; }; + +class MockAccessibilityBridgeNoWindow : public AccessibilityBridgeIos { + public: + MockAccessibilityBridgeNoWindow() : observations({}) { + view_ = [[UIView alloc] initWithFrame:kScreenSize]; + } + bool isVoiceOverRunning() const override { return isVoiceOverRunningValue; } + UIView* view() const override { return view_; } + UIView* textInputView() override { return nil; } + void DispatchSemanticsAction(int32_t id, SemanticsAction action) override { + SemanticsActionObservation observation(id, action); + observations.push_back(observation); + } + void DispatchSemanticsAction(int32_t id, + SemanticsAction action, + fml::MallocMapping args) override { + SemanticsActionObservation observation(id, action); + observations.push_back(observation); + } + void AccessibilityObjectDidBecomeFocused(int32_t id) override {} + void AccessibilityObjectDidLoseFocus(int32_t id) override {} + std::shared_ptr GetPlatformViewsController() const override { + return nil; + } + std::vector observations; + bool isVoiceOverRunningValue; + + private: + UIView* view_; +}; } // namespace } // namespace flutter @@ -208,6 +238,46 @@ - (void)testVerticalFlutterScrollableSemanticsObject { CGPointMake(0, scrollPosition * effectivelyScale))); } +- (void)testVerticalFlutterScrollableSemanticsObjectNoWindow { + fml::WeakPtrFactory factory( + new flutter::MockAccessibilityBridgeNoWindow()); + fml::WeakPtr bridge = factory.GetWeakPtr(); + + float transformScale = 0.5f; + float screenScale = + [UIScreen mainScreen].scale; // Flutter view without window uses [UIScreen mainScreen]; + float effectivelyScale = transformScale / screenScale; + float x = 10; + float y = 10; + float w = 100; + float h = 200; + float scrollExtentMax = 500.0; + float scrollPosition = 150.0; + + flutter::SemanticsNode node; + node.flags = static_cast(flutter::SemanticsFlags::kHasImplicitScrolling); + node.actions = flutter::kVerticalScrollSemanticsActions; + node.rect = SkRect::MakeXYWH(x, y, w, h); + node.scrollExtentMax = scrollExtentMax; + node.scrollPosition = scrollPosition; + node.transform = { + transformScale, 0, 0, 0, 0, transformScale, 0, 0, 0, 0, transformScale, 0, 0, 0, 0, 1.0}; + FlutterSemanticsObject* delegate = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0]; + FlutterScrollableSemanticsObject* scrollable = + [[FlutterScrollableSemanticsObject alloc] initWithSemanticsObject:delegate]; + SemanticsObject* scrollable_object = static_cast(scrollable); + [scrollable_object setSemanticsNode:&node]; + [scrollable_object accessibilityBridgeDidFinishUpdate]; + XCTAssertTrue( + CGRectEqualToRect(scrollable.frame, CGRectMake(x * effectivelyScale, y * effectivelyScale, + w * effectivelyScale, h * effectivelyScale))); + XCTAssertTrue(CGSizeEqualToSize( + scrollable.contentSize, + CGSizeMake(w * effectivelyScale, (h + scrollExtentMax) * effectivelyScale))); + XCTAssertTrue(CGPointEqualToPoint(scrollable.contentOffset, + CGPointMake(0, scrollPosition * effectivelyScale))); +} + - (void)testHorizontalFlutterScrollableSemanticsObject { fml::WeakPtrFactory factory( new flutter::MockAccessibilityBridge());