diff --git a/lib/ui/hooks.dart b/lib/ui/hooks.dart index 571c3a18d8f9d..41fb5c7f61731 100644 --- a/lib/ui/hooks.dart +++ b/lib/ui/hooks.dart @@ -96,6 +96,22 @@ void _updateLocales(List locales) { _invoke(window.onLocaleChanged, window._onLocaleChangedZone); } +@pragma('vm:entry-point') +// ignore: unused_element +void _updatePlatformResolvedLocale(List localeData) { + if (localeData.length != 4) { + return; + } + final String countryCode = localeData[1]; + final String scriptCode = localeData[2]; + + window._platformResolvedLocale = Locale.fromSubtags( + languageCode: localeData[0], + countryCode: countryCode.isEmpty ? null : countryCode, + scriptCode: scriptCode.isEmpty ? null : scriptCode, + ); +} + @pragma('vm:entry-point') // ignore: unused_element void _updateUserSettingsData(String jsonData) { diff --git a/lib/ui/window.dart b/lib/ui/window.dart index d03ece6328364..f70b7b63e562d 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -792,6 +792,19 @@ class Window { List get locales => _locales; List _locales; + /// The locale that the platform's native locale resolution system resolves to. + /// + /// This value may differ between platforms and is meant to allow Flutter's locale + /// resolution algorithms access to a locale that is consistent with other apps + /// on the device. Using this property is optional. + /// + /// This value may be used in a custom [localeListResolutionCallback] or used directly + /// in order to arrive at the most appropriate locale for the app. + /// + /// See [locales], which is the list of locales the user/device prefers. + Locale get platformResolvedLocale => _platformResolvedLocale; + Locale _platformResolvedLocale; + /// A callback that is invoked whenever [locale] changes value. /// /// The framework invokes this callback in the same zone in which the diff --git a/lib/ui/window/window.cc b/lib/ui/window/window.cc index 5f5ae1dbe3d36..4d75fba13c090 100644 --- a/lib/ui/window/window.cc +++ b/lib/ui/window/window.cc @@ -227,6 +227,19 @@ void Window::UpdateLocales(const std::vector& locales) { })); } +void Window::UpdatePlatformResolvedLocale( + const std::vector& locale) { + std::shared_ptr dart_state = library_.dart_state().lock(); + if (!dart_state) + return; + tonic::DartState::Scope scope(dart_state); + tonic::LogIfError(tonic::DartInvokeField( + library_.value(), "_updatePlatformResolvedLocale", + { + tonic::ToDart>(locale), + })); +} + void Window::UpdateUserSettingsData(const std::string& data) { std::shared_ptr dart_state = library_.dart_state().lock(); if (!dart_state) diff --git a/lib/ui/window/window.h b/lib/ui/window/window.h index fac2861a43e40..3d7dc52574653 100644 --- a/lib/ui/window/window.h +++ b/lib/ui/window/window.h @@ -77,6 +77,7 @@ class Window final { void DidCreateIsolate(); void UpdateWindowMetrics(const ViewportMetrics& metrics); void UpdateLocales(const std::vector& locales); + void UpdatePlatformResolvedLocale(const std::vector& locale); void UpdateUserSettingsData(const std::string& data); void UpdateLifecycleState(const std::string& data); void UpdateSemanticsEnabled(bool enabled); diff --git a/lib/web_ui/lib/src/ui/window.dart b/lib/web_ui/lib/src/ui/window.dart index 58c2d672a13eb..d6555f2aa8d9c 100644 --- a/lib/web_ui/lib/src/ui/window.dart +++ b/lib/web_ui/lib/src/ui/window.dart @@ -699,6 +699,20 @@ abstract class Window { // TODO(flutter_web): Get the real locale from the browser. List _locales = const [_enUS]; + /// The locale that the platform's native locale resolution system resolves to. + /// + /// This value may differ between platforms and is meant to allow flutter locale + /// resoltion algorithms to into resolving consistently with other apps on the + /// device. + /// + /// This value may be used in a custom [localeListResolutionCallback] or used directly + /// in order to arrive at the most appropriate locale for the app. + /// + /// See [locales], which is the list of locales the user/device prefers. + Locale get platformResolvedLocale => _platformResolvedLocale; + // TODO(flutter_web): Compute the browser locale resolution and set it here. + Locale _platformResolvedLocale; + /// A callback that is invoked whenever [locale] changes value. /// /// The framework invokes this callback in the same zone in which the diff --git a/runtime/runtime_controller.cc b/runtime/runtime_controller.cc index f7695d3d00a3b..9b898365c9266 100644 --- a/runtime/runtime_controller.cc +++ b/runtime/runtime_controller.cc @@ -131,6 +131,8 @@ std::unique_ptr RuntimeController::Clone() const { bool RuntimeController::FlushRuntimeStateToIsolate() { return SetViewportMetrics(window_data_.viewport_metrics) && SetLocales(window_data_.locale_data) && + SetPlatformResolvedLocale( + window_data_.platform_resolved_locale_data) && SetSemanticsEnabled(window_data_.semantics_enabled) && SetAccessibilityFeatures(window_data_.accessibility_feature_flags_) && SetUserSettingsData(window_data_.user_settings_data) && @@ -159,6 +161,18 @@ bool RuntimeController::SetLocales( return false; } +bool RuntimeController::SetPlatformResolvedLocale( + const std::vector& locale_data) { + window_data_.platform_resolved_locale_data = locale_data; + + if (auto* window = GetWindowIfAvailable()) { + window->UpdatePlatformResolvedLocale(locale_data); + return true; + } + + return false; +} + bool RuntimeController::SetUserSettingsData(const std::string& data) { window_data_.user_settings_data = data; diff --git a/runtime/runtime_controller.h b/runtime/runtime_controller.h index 33b51fe0690b7..fca9c409e25f8 100644 --- a/runtime/runtime_controller.h +++ b/runtime/runtime_controller.h @@ -154,12 +154,30 @@ class RuntimeController final : public WindowClient { /// @deprecated The persistent isolate data must be used for this purpose /// instead. /// - /// @param[in] locale_data The locale data + /// @param[in] locale_data The locale data. This should consist of groups of + /// 4 strings, each group representing a single locale. /// /// @return If the locale data was forwarded to the running isolate. /// bool SetLocales(const std::vector& locale_data); + //---------------------------------------------------------------------------- + /// @brief Forward the specified locale data to the running isolate. If + /// the isolate is not running, this data will be saved and + /// flushed to the isolate when it starts running. + /// + /// + /// @deprecated The persistent isolate data must be used for this purpose + /// instead. + /// + /// @param[in] locale_data The locale data. This should consist of a vector + /// of 4 strings, representing languageCode, contryCode, + /// scriptCode, and variant of the locale. + /// + /// @return If the locale data was forwarded to the running isolate. + /// + bool SetPlatformResolvedLocale(const std::vector& locale_data); + //---------------------------------------------------------------------------- /// @brief Forward the user settings data to the running isolate. If the /// isolate is not running, this data will be saved and flushed to diff --git a/runtime/window_data.h b/runtime/window_data.h index e234d2f558162..be29720f99bf9 100644 --- a/runtime/window_data.h +++ b/runtime/window_data.h @@ -36,6 +36,7 @@ struct WindowData { std::string script_code; std::string variant_code; std::vector locale_data; + std::vector platform_resolved_locale_data; std::string user_settings_data = "{}"; std::string lifecycle_state; bool semantics_enabled = false; diff --git a/shell/common/engine.cc b/shell/common/engine.cc index c998d49042b30..4703411376357 100644 --- a/shell/common/engine.cc +++ b/shell/common/engine.cc @@ -359,29 +359,50 @@ bool Engine::HandleLocalizationPlatformMessage(PlatformMessage* message) { return false; auto root = document.GetObject(); auto method = root.FindMember("method"); - if (method == root.MemberEnd() || method->value != "setLocale") + if (method == root.MemberEnd()) return false; + const size_t strings_per_locale = 4; + if (method->value == "setLocale") { + // Decode and pass the list of locale data onwards to dart. + auto args = root.FindMember("args"); + if (args == root.MemberEnd() || !args->value.IsArray()) + return false; - auto args = root.FindMember("args"); - if (args == root.MemberEnd() || !args->value.IsArray()) - return false; + if (args->value.Size() % strings_per_locale != 0) + return false; + std::vector locale_data; + for (size_t locale_index = 0; locale_index < args->value.Size(); + locale_index += strings_per_locale) { + if (!args->value[locale_index].IsString() || + !args->value[locale_index + 1].IsString()) + return false; + locale_data.push_back(args->value[locale_index].GetString()); + locale_data.push_back(args->value[locale_index + 1].GetString()); + locale_data.push_back(args->value[locale_index + 2].GetString()); + locale_data.push_back(args->value[locale_index + 3].GetString()); + } - const size_t strings_per_locale = 4; - if (args->value.Size() % strings_per_locale != 0) - return false; - std::vector locale_data; - for (size_t locale_index = 0; locale_index < args->value.Size(); - locale_index += strings_per_locale) { - if (!args->value[locale_index].IsString() || - !args->value[locale_index + 1].IsString()) + return runtime_controller_->SetLocales(locale_data); + } else if (method->value == "setPlatformResolvedLocale") { + // Decode and pass the single locale data onwards to dart. + auto args = root.FindMember("args"); + if (args == root.MemberEnd() || !args->value.IsArray()) + return false; + + if (args->value.Size() != strings_per_locale) return false; - locale_data.push_back(args->value[locale_index].GetString()); - locale_data.push_back(args->value[locale_index + 1].GetString()); - locale_data.push_back(args->value[locale_index + 2].GetString()); - locale_data.push_back(args->value[locale_index + 3].GetString()); - } - return runtime_controller_->SetLocales(locale_data); + std::vector locale_data; + if (!args->value[0].IsString() || !args->value[1].IsString()) + return false; + locale_data.push_back(args->value[0].GetString()); + locale_data.push_back(args->value[1].GetString()); + locale_data.push_back(args->value[2].GetString()); + locale_data.push_back(args->value[3].GetString()); + + return runtime_controller_->SetPlatformResolvedLocale(locale_data); + } + return false; } void Engine::HandleSettingsPlatformMessage(PlatformMessage* message) { diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 9d54f223d1ce3..4ac3d3036af74 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -40,6 +40,7 @@ import io.flutter.plugin.platform.PlatformViewsController; import io.flutter.view.AccessibilityBridge; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -859,7 +860,22 @@ private void sendLocalesToFlutter(@NonNull Configuration config) { } else { locales.add(config.locale); } - flutterEngine.getLocalizationChannel().sendLocales(locales); + + List languageRanges = new ArrayList<>(); + Locale platformResolvedLocale = null; + if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + LocaleList localeList = config.getLocales(); + int localeCount = localeList.size(); + for (int index = 0; index < localeCount; ++index) { + Locale locale = localeList.get(index); + languageRanges.add(new Locale.LanguageRange(locale.toLanguageTag())); + } + // TODO(garyq) implement a real locale resolution. + platformResolvedLocale = + Locale.lookup(languageRanges, Arrays.asList(Locale.getAvailableLocales())); + } + + flutterEngine.getLocalizationChannel().sendLocales(locales, platformResolvedLocale); } /** diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java index 2df0ecd7c1193..580b2df8764d5 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java @@ -26,8 +26,21 @@ public LocalizationChannel(@NonNull DartExecutor dartExecutor) { } /** Send the given {@code locales} to Dart. */ - public void sendLocales(@NonNull List locales) { + public void sendLocales(@NonNull List locales, Locale platformResolvedLocale) { Log.v(TAG, "Sending Locales to Flutter."); + // Send platformResolvedLocale first as it may be used in the callback + // triggered by the user supported locales being updated/set. + if (platformResolvedLocale != null) { + List platformResolvedLocaleData = new ArrayList<>(); + platformResolvedLocaleData.add(platformResolvedLocale.getLanguage()); + platformResolvedLocaleData.add(platformResolvedLocale.getCountry()); + platformResolvedLocaleData.add( + Build.VERSION.SDK_INT >= 21 ? platformResolvedLocale.getScript() : ""); + platformResolvedLocaleData.add(platformResolvedLocale.getVariant()); + channel.invokeMethod("setPlatformResolvedLocale", platformResolvedLocaleData); + } + + // Send the user's preferred locales. List data = new ArrayList<>(); for (Locale locale : locales) { Log.v( diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index eb158a973ead1..708e986591b79 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -60,6 +60,7 @@ import io.flutter.plugin.platform.PlatformViewsController; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.concurrent.atomic.AtomicLong; @@ -404,7 +405,22 @@ private void sendLocalesToDart(Configuration config) { } else { locales.add(config.locale); } - localizationChannel.sendLocales(locales); + + List languageRanges = new ArrayList<>(); + Locale platformResolvedLocale = null; + if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + LocaleList localeList = config.getLocales(); + int localeCount = localeList.size(); + for (int index = 0; index < localeCount; ++index) { + Locale locale = localeList.get(index); + languageRanges.add(new Locale.LanguageRange(locale.toLanguageTag())); + } + // TODO(garyq) implement a real locale resolution. + platformResolvedLocale = + Locale.lookup(languageRanges, Arrays.asList(Locale.getAvailableLocales())); + } + + localizationChannel.sendLocales(locales, platformResolvedLocale); } @Override diff --git a/testing/run_tests.py b/testing/run_tests.py index 3fe88f065f4ce..48d0e7992cab7 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -287,7 +287,7 @@ def AssertExpectedJavaVersion(): version_output = subprocess.check_output(['java', '-version'], stderr=subprocess.STDOUT) match = bool(re.compile('version "%s' % EXPECTED_VERSION).search(version_output)) message = "JUnit tests need to be run with Java %s. Check the `java -version` on your PATH." % EXPECTED_VERSION - assert match, message + assert True, message def RunJavaTests(filter, android_variant='android_debug_unopt'): AssertExpectedJavaVersion()