diff --git a/engine/src/flutter/lib/ui/hooks.dart b/engine/src/flutter/lib/ui/hooks.dart index cb5a8738f5e..f833c1aeacb 100644 --- a/engine/src/flutter/lib/ui/hooks.dart +++ b/engine/src/flutter/lib/ui/hooks.dart @@ -32,11 +32,21 @@ void _updateLocale(String languageCode, String countryCode) { _invoke(window.onLocaleChanged, window._onLocaleChangedZone); } +void _updateUserSettingsData(String json) { + final Map data = JSON.decode(json); + _updateTextScaleFactor(data['textScaleFactor'].toDouble()); + _updateAlwaysUse24HourFormat(data['alwaysUse24HourFormat']); +} + void _updateTextScaleFactor(double textScaleFactor) { window._textScaleFactor = textScaleFactor; _invoke(window.onTextScaleFactorChanged, window._onTextScaleFactorChangedZone); } +void _updateAlwaysUse24HourFormat(bool alwaysUse24HourFormat) { + window._alwaysUse24HourFormat = alwaysUse24HourFormat; +} + void _updateSemanticsEnabled(bool enabled) { window._semanticsEnabled = enabled; _invoke(window.onSemanticsEnabledChanged, window._onSemanticsEnabledChangedZone); diff --git a/engine/src/flutter/lib/ui/window.dart b/engine/src/flutter/lib/ui/window.dart index 97be8525379..421368cba7c 100644 --- a/engine/src/flutter/lib/ui/window.dart +++ b/engine/src/flutter/lib/ui/window.dart @@ -269,6 +269,13 @@ class Window { double get textScaleFactor => _textScaleFactor; double _textScaleFactor = 1.0; + /// The setting indicating whether time should always be shown in the 24-hour + /// format. + /// + /// This option is used by [showTimePicker]. + bool get alwaysUse24HourFormat => _alwaysUse24HourFormat; + bool _alwaysUse24HourFormat = false; + /// A callback that is invoked whenever [textScaleFactor] changes value. /// /// The framework invokes this callback in the same zone in which the diff --git a/engine/src/flutter/lib/ui/window/window.cc b/engine/src/flutter/lib/ui/window/window.cc index b8429804430..91114b0a72c 100644 --- a/engine/src/flutter/lib/ui/window/window.cc +++ b/engine/src/flutter/lib/ui/window/window.cc @@ -163,15 +163,15 @@ void Window::UpdateLocale(const std::string& language_code, }); } -void Window::UpdateTextScaleFactor(double text_scale_factor) { +void Window::UpdateUserSettingsData(const std::string& data) { tonic::DartState* dart_state = library_.dart_state().get(); if (!dart_state) return; tonic::DartState::Scope scope(dart_state); - DartInvokeField(library_.value(), "_updateTextScaleFactor", + DartInvokeField(library_.value(), "_updateUserSettingsData", { - ToDart(static_cast(text_scale_factor)), + StdStringToDart(data), }); } diff --git a/engine/src/flutter/lib/ui/window/window.h b/engine/src/flutter/lib/ui/window/window.h index df590195447..ac65feaf26c 100644 --- a/engine/src/flutter/lib/ui/window/window.h +++ b/engine/src/flutter/lib/ui/window/window.h @@ -45,7 +45,7 @@ class Window { void UpdateWindowMetrics(const ViewportMetrics& metrics); void UpdateLocale(const std::string& language_code, const std::string& country_code); - void UpdateTextScaleFactor(double text_scale_factor); + void UpdateUserSettingsData(const std::string& data); void UpdateSemanticsEnabled(bool enabled); void DispatchPlatformMessage(fxl::RefPtr message); void DispatchPointerDataPacket(const PointerDataPacket& packet); diff --git a/engine/src/flutter/runtime/runtime_controller.cc b/engine/src/flutter/runtime/runtime_controller.cc index 37908815834..67f34ad9f1e 100644 --- a/engine/src/flutter/runtime/runtime_controller.cc +++ b/engine/src/flutter/runtime/runtime_controller.cc @@ -66,11 +66,11 @@ void RuntimeController::SetLocale(const std::string& language_code, GetWindow()->UpdateLocale(language_code_, country_code_); } -void RuntimeController::SetTextScaleFactor(double text_scale_factor) { - if (text_scale_factor_ == text_scale_factor) +void RuntimeController::SetUserSettingsData(const std::string& data) { + if (user_settings_data_ == data) return; - text_scale_factor_ = text_scale_factor; - GetWindow()->UpdateTextScaleFactor(text_scale_factor_); + user_settings_data_ = data; + GetWindow()->UpdateUserSettingsData(user_settings_data_); } void RuntimeController::SetSemanticsEnabled(bool enabled) { diff --git a/engine/src/flutter/runtime/runtime_controller.h b/engine/src/flutter/runtime/runtime_controller.h index a09a7c7dd0c..f29ee60f753 100644 --- a/engine/src/flutter/runtime/runtime_controller.h +++ b/engine/src/flutter/runtime/runtime_controller.h @@ -35,7 +35,7 @@ class RuntimeController : public WindowClient, public IsolateClient { void SetViewportMetrics(const ViewportMetrics& metrics); void SetLocale(const std::string& language_code, const std::string& country_code); - void SetTextScaleFactor(double textScaleFactor); + void SetUserSettingsData(const std::string& data); void SetSemanticsEnabled(bool enabled); void BeginFrame(fxl::TimePoint frame_time); @@ -66,7 +66,7 @@ class RuntimeController : public WindowClient, public IsolateClient { RuntimeDelegate* client_; std::string language_code_; std::string country_code_; - double text_scale_factor_ = 1.0; + std::string user_settings_data_ = "{}"; bool semantics_enabled_ = false; std::unique_ptr dart_controller_; diff --git a/engine/src/flutter/shell/common/engine.cc b/engine/src/flutter/shell/common/engine.cc index 1ae2762dcbb..7439d38dc58 100644 --- a/engine/src/flutter/shell/common/engine.cc +++ b/engine/src/flutter/shell/common/engine.cc @@ -44,7 +44,7 @@ constexpr char kAssetChannel[] = "flutter/assets"; constexpr char kLifecycleChannel[] = "flutter/lifecycle"; constexpr char kNavigationChannel[] = "flutter/navigation"; constexpr char kLocalizationChannel[] = "flutter/localization"; -constexpr char kSystemChannel[] = "flutter/system"; +constexpr char kSettingsChannel[] = "flutter/settings"; bool PathExists(const std::string& path) { return access(path.c_str(), R_OK) == 0; @@ -75,7 +75,7 @@ Engine::Engine(PlatformView* platform_view) platform_view->GetVsyncWaiter(), this)), load_script_error_(tonic::kNoError), - text_scale_factor_(1.0), + user_settings_data_("{}"), activity_running_(false), have_surface_(false), weak_factory_(this) {} @@ -337,11 +337,9 @@ void Engine::DispatchPlatformMessage( } else if (message->channel() == kLocalizationChannel) { if (HandleLocalizationPlatformMessage(message.get())) return; - } else if (message->channel() == kSystemChannel) { - // This only handles textScaleFactor changes: other system messages are - // handled by DispatchPlatformMessage below. - if (HandleSystemPlatformMessage(message.get())) - return; + } else if (message->channel() == kSettingsChannel) { + HandleSettingsPlatformMessage(message.get()); + return; } if (runtime_) { @@ -424,35 +422,15 @@ bool Engine::HandleLocalizationPlatformMessage( return true; } -bool Engine::HandleSystemPlatformMessage(blink::PlatformMessage* message) { +void Engine::HandleSettingsPlatformMessage(blink::PlatformMessage* message) { const auto& data = message->data(); - rapidjson::Document document; - document.Parse(reinterpret_cast(data.data()), data.size()); - - if (document.HasParseError() || !document.IsObject()) - return false; - - auto root = document.GetObject(); - auto type = root.FindMember("type"); - if (type == root.MemberEnd() || type->value != "systemSettings") - return false; - - // This only handles textScaleFactor changes: other system messages - // are handled by DispatchPlatformMessage. - auto text_scale_factor = root.FindMember("textScaleFactor"); - if (text_scale_factor == root.MemberEnd() || - !text_scale_factor->value.IsDouble()) { - return false; - } - text_scale_factor_ = text_scale_factor->value.GetDouble(); + std::string jsonData(reinterpret_cast(data.data()), data.size()); + user_settings_data_ = jsonData; if (runtime_) { - runtime_->SetTextScaleFactor(text_scale_factor_); + runtime_->SetUserSettingsData(user_settings_data_); if (have_surface_) ScheduleFrame(); } - // If the only members were "type" and "textScaleFactor", then we're done. - // If there are more members, then we need to send it on to other handlers. - return root.MemberCount() == 2; } void Engine::DispatchPointerDataPacket(const PointerDataPacket& packet) { @@ -507,7 +485,7 @@ void Engine::ConfigureRuntime(const std::string& script_uri, default_isolate_snapshot_instr, platform_kernel); runtime_->SetViewportMetrics(viewport_metrics_); runtime_->SetLocale(language_code_, country_code_); - runtime_->SetTextScaleFactor(text_scale_factor_); + runtime_->SetUserSettingsData(user_settings_data_); runtime_->SetSemanticsEnabled(semantics_enabled_); } } diff --git a/engine/src/flutter/shell/common/engine.h b/engine/src/flutter/shell/common/engine.h index 5a413fa42c1..4cee55554a2 100644 --- a/engine/src/flutter/shell/common/engine.h +++ b/engine/src/flutter/shell/common/engine.h @@ -97,7 +97,7 @@ class Engine : public blink::RuntimeDelegate { bool HandleNavigationPlatformMessage( fxl::RefPtr message); bool HandleLocalizationPlatformMessage(blink::PlatformMessage* message); - bool HandleSystemPlatformMessage(blink::PlatformMessage* message); + void HandleSettingsPlatformMessage(blink::PlatformMessage* message); void HandleAssetPlatformMessage(fxl::RefPtr message); bool GetAssetAsBuffer(const std::string& name, std::vector* data); @@ -112,7 +112,7 @@ class Engine : public blink::RuntimeDelegate { blink::ViewportMetrics viewport_metrics_; std::string language_code_; std::string country_code_; - double text_scale_factor_; + std::string user_settings_data_; bool semantics_enabled_ = false; // TODO(abarth): Unify these two behind a common interface. fxl::RefPtr asset_store_; diff --git a/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java b/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java index bd16aa7770c..c742acc1907 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java @@ -17,6 +17,7 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Matrix; import android.os.Build; +import android.text.format.DateFormat; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; @@ -101,6 +102,7 @@ public class FlutterView extends SurfaceView private final BasicMessageChannel mFlutterKeyEventChannel; private final BasicMessageChannel mFlutterLifecycleChannel; private final BasicMessageChannel mFlutterSystemChannel; + private final BasicMessageChannel mFlutterSettingsChannel; private final BroadcastReceiver mDiscoveryReceiver; private final List mActivityLifecycleListeners; private final List mFirstFrameListeners; @@ -173,6 +175,8 @@ public class FlutterView extends SurfaceView StringCodec.INSTANCE); mFlutterSystemChannel = new BasicMessageChannel<>(this, "flutter/system", JSONMessageCodec.INSTANCE); + mFlutterSettingsChannel = new BasicMessageChannel<>(this, "flutter/settings", + JSONMessageCodec.INSTANCE); // TODO(plugins): Change PlatformPlugin to accept a Context. Disable the // operations that require an Activity when a Context is passed. @@ -186,7 +190,7 @@ public class FlutterView extends SurfaceView mTextInputPlugin = new TextInputPlugin(this); setLocale(getResources().getConfiguration().locale); - setTextScaleFactor(getResources().getConfiguration().fontScale); + setUserSettings(); if ((context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { mDiscoveryReceiver = new DiscoveryReceiver(); @@ -284,11 +288,11 @@ public class FlutterView extends SurfaceView mFlutterNavigationChannel.invokeMethod("popRoute", null); } - private void setTextScaleFactor(float textScaleFactor) { + private void setUserSettings() { Map message = new HashMap<>(); - message.put("type", "systemSettings"); - message.put("textScaleFactor", textScaleFactor); - mFlutterSystemChannel.send(message); + message.put("textScaleFactor", getResources().getConfiguration().fontScale); + message.put("alwaysUse24HourFormat", DateFormat.is24HourFormat(getContext())); + mFlutterSettingsChannel.send(message); } private void setLocale(Locale locale) { @@ -300,7 +304,7 @@ public class FlutterView extends SurfaceView protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); setLocale(newConfig.locale); - setTextScaleFactor(newConfig.fontScale); + setUserSettings(); } float getDevicePixelRatio() { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 05dcabdae57..855283f623f 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -72,6 +72,7 @@ class PlatformMessageResponseDarwin : public blink::PlatformMessageResponse { fml::scoped_nsprotocol _textInputChannel; fml::scoped_nsprotocol _lifecycleChannel; fml::scoped_nsprotocol _systemChannel; + fml::scoped_nsprotocol _settingsChannel; fml::scoped_nsprotocol _launchView; bool _platformSupportsTouchTypes; bool _platformSupportsTouchPressure; @@ -177,6 +178,11 @@ class PlatformMessageResponseDarwin : public blink::PlatformMessageResponse { binaryMessenger:self codec:[FlutterJSONMessageCodec sharedInstance]]); + _settingsChannel.reset([[FlutterBasicMessageChannel alloc] + initWithName:@"flutter/settings" + binaryMessenger:self + codec:[FlutterJSONMessageCodec sharedInstance]]); + _platformPlugin.reset([[FlutterPlatformPlugin alloc] init]); [_platformChannel.get() setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { [_platformPlugin.get() handleMethodCall:call result:result]; @@ -247,6 +253,11 @@ class PlatformMessageResponseDarwin : public blink::PlatformMessageResponse { selector:@selector(onMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; + + [center addObserver:self + selector:@selector(onUserSettingsChanged:) + name:UIContentSizeCategoryDidChangeNotification + object:nil]; } - (void)setInitialRoute:(NSString*)route { @@ -340,6 +351,7 @@ class PlatformMessageResponseDarwin : public blink::PlatformMessageResponse { - (void)viewDidAppear:(BOOL)animated { TRACE_EVENT0("flutter", "viewDidAppear"); [self onLocaleUpdated:nil]; + [self onUserSettingsChanged:nil]; [self onVoiceOverChanged:nil]; [_lifecycleChannel.get() sendMessage:@"AppLifecycleState.resumed"]; @@ -693,6 +705,60 @@ static inline blink::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* to [_localizationChannel.get() invokeMethod:@"setLocale" arguments:@[ languageCode, countryCode ]]; } +#pragma mark - Set user settings + +- (void)onUserSettingsChanged:(NSNotification*)notification { + [_settingsChannel.get() sendMessage:@{ + @"textScaleFactor" : @([self textScaleFactor]), + @"alwaysUse24HourFormat" : @([self isAlwaysUse24HourFormat]), + }]; +} + +- (CGFloat)textScaleFactor { + UIContentSizeCategory category = [UIApplication sharedApplication].preferredContentSizeCategory; + // The delta is computed based on the following: + // - L (large) is the default 1.0 scale. + // - The scale is linear spanning from XS to XXXL. + // - XXXL = 1.4 * XS. + // + // L = 1.0 = XS + 3 * delta + // XXXL = 1.4 * XS = XS + 6 * delta + const CGFloat delta = 0.055555; + if ([category isEqualToString:UIContentSizeCategoryExtraSmall]) + return 1.0 - 3 * delta; + else if ([category isEqualToString:UIContentSizeCategorySmall]) + return 1.0 - 2 * delta; + else if ([category isEqualToString:UIContentSizeCategoryMedium]) + return 1.0 - delta; + else if ([category isEqualToString:UIContentSizeCategoryLarge]) + return 1.0; + else if ([category isEqualToString:UIContentSizeCategoryExtraLarge]) + return 1.0 + delta; + else if ([category isEqualToString:UIContentSizeCategoryExtraExtraLarge]) + return 1.0 + 2 * delta; + else if ([category isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) + return 1.0 + 3 * delta; + else + return 1.0; +} + +- (BOOL)isAlwaysUse24HourFormat { + // iOS does not report its "24-Hour Time" user setting in the API. Instead, it applies + // it automatically to NSDateFormatter when used with [NSLocale currentLocale]. It is + // essential that [NSLocale currentLocale] is used. Any custom locale, even the one + // that's the same as [NSLocale currentLocale] will ignore the 24-hour option (there + // must be some internal field that's not exposed to developers). + // + // Therefore this option behaves differently across Android and iOS. On Android this + // setting is exposed standalone, and can therefore be applied to all locales, whether + // the "current system locale" or a custom one. On iOS it only applies to the current + // system locale. Widget implementors must take this into account in order to provide + // platform-idiomatic behavior in their widgets. + NSString* dateFormat = + [NSDateFormatter dateFormatFromTemplate:@"j" options:0 locale:[NSLocale currentLocale]]; + return [dateFormat rangeOfString:@"a"].location == NSNotFound; +} + #pragma mark - Status Bar touch event handling // Standard iOS status bar height in pixels.