diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index cc47ffb72f8ce..19dda64a34c67 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -762,6 +762,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/StringCod FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/localization/LocalizationPlugin.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/mouse/MouseCursorPlugin.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/AccessibilityEventsDelegate.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java diff --git a/lib/ui/window.dart b/lib/ui/window.dart index 3d593a8269111..f6c2b2aed4ac8 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -809,6 +809,34 @@ class Window { Locale? get platformResolvedLocale => _platformResolvedLocale; Locale? _platformResolvedLocale; + /// Performs the platform-native locale resolution. + /// + /// Each platform may return different results. + /// + /// If the platform fails to resolve a locale, then this will return null. + /// + /// This method returns synchronously and is a direct call to + /// platform specific APIs without invoking method channels. + Locale? computePlatformResolvedLocale(List supportedLocales) { + final List supportedLocalesData = []; + for (Locale locale in supportedLocales) { + supportedLocalesData.add(locale.languageCode); + supportedLocalesData.add(locale.countryCode); + supportedLocalesData.add(locale.scriptCode); + } + + final List result = _computePlatformResolvedLocale(supportedLocalesData); + + if (result.isNotEmpty) { + return Locale.fromSubtags( + languageCode: result[0], + countryCode: result[1] == '' ? null : result[1], + scriptCode: result[2] == '' ? null : result[2]); + } + return null; + } + List _computePlatformResolvedLocale(List supportedLocalesData) native 'Window_computePlatformResolvedLocale'; + /// 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 8e44378a6f3fa..0ddcb0258533e 100644 --- a/lib/ui/window/window.cc +++ b/lib/ui/window/window.cc @@ -425,6 +425,27 @@ void Window::CompletePlatformMessageResponse(int response_id, response->Complete(std::make_unique(std::move(data))); } +Dart_Handle ComputePlatformResolvedLocale(Dart_Handle supportedLocalesHandle) { + std::vector supportedLocales = + tonic::DartConverter>::FromDart( + supportedLocalesHandle); + + std::vector results = + *UIDartState::Current() + ->window() + ->client() + ->ComputePlatformResolvedLocale(supportedLocales); + + return tonic::DartConverter>::ToDart(results); +} + +static void _ComputePlatformResolvedLocale(Dart_NativeArguments args) { + UIDartState::ThrowIfUIOperationsProhibited(); + Dart_Handle result = + ComputePlatformResolvedLocale(Dart_GetNativeArgument(args, 1)); + Dart_SetReturnValue(args, result); +} + void Window::RegisterNatives(tonic::DartLibraryNatives* natives) { natives->Register({ {"Window_defaultRouteName", DefaultRouteName, 1, true}, @@ -437,6 +458,8 @@ void Window::RegisterNatives(tonic::DartLibraryNatives* natives) { {"Window_reportUnhandledException", ReportUnhandledException, 2, true}, {"Window_setNeedsReportTimings", SetNeedsReportTimings, 2, true}, {"Window_getPersistentIsolateData", GetPersistentIsolateData, 1, true}, + {"Window_computePlatformResolvedLocale", _ComputePlatformResolvedLocale, + 2, true}, }); } diff --git a/lib/ui/window/window.h b/lib/ui/window/window.h index 3d7dc52574653..dd5bd28daa0de 100644 --- a/lib/ui/window/window.h +++ b/lib/ui/window/window.h @@ -59,6 +59,9 @@ class WindowClient { int64_t isolate_port) = 0; virtual void SetNeedsReportTimings(bool value) = 0; virtual std::shared_ptr GetPersistentIsolateData() = 0; + virtual std::unique_ptr> + ComputePlatformResolvedLocale( + const std::vector& supported_locale_data) = 0; protected: virtual ~WindowClient(); diff --git a/lib/web_ui/lib/src/ui/window.dart b/lib/web_ui/lib/src/ui/window.dart index 0b8992032d85e..023a6ebde9b12 100644 --- a/lib/web_ui/lib/src/ui/window.dart +++ b/lib/web_ui/lib/src/ui/window.dart @@ -616,6 +616,19 @@ abstract class Window { /// See [locales], which is the list of locales the user/device prefers. Locale/*?*/ get platformResolvedLocale; + /// Performs the platform-native locale resolution. + /// + /// Each platform may return different results. + /// + /// If the platform fails to resolve a locale, then this will return null. + /// + /// This method returns synchronously and is a direct call to + /// platform specific APIs without invoking method channels. + Locale computePlatformResolvedLocale(List supportedLocales) { + // TODO(garyq): Implement on web. + return null; + } + /// 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 9b898365c9266..66a57994705de 100644 --- a/runtime/runtime_controller.cc +++ b/runtime/runtime_controller.cc @@ -340,6 +340,13 @@ RuntimeController::GetPersistentIsolateData() { return persistent_isolate_data_; } +// |WindowClient| +std::unique_ptr> +RuntimeController::ComputePlatformResolvedLocale( + const std::vector& supported_locale_data) { + return client_.ComputePlatformResolvedLocale(supported_locale_data); +} + Dart_Port RuntimeController::GetMainPort() { std::shared_ptr root_isolate = root_isolate_.lock(); return root_isolate ? root_isolate->main_port() : ILLEGAL_PORT; diff --git a/runtime/runtime_controller.h b/runtime/runtime_controller.h index fca9c409e25f8..3ac276c624746 100644 --- a/runtime/runtime_controller.h +++ b/runtime/runtime_controller.h @@ -522,6 +522,10 @@ class RuntimeController final : public WindowClient { // |WindowClient| std::shared_ptr GetPersistentIsolateData() override; + // |WindowClient| + std::unique_ptr> ComputePlatformResolvedLocale( + const std::vector& supported_locale_data) override; + FML_DISALLOW_COPY_AND_ASSIGN(RuntimeController); }; diff --git a/runtime/runtime_delegate.h b/runtime/runtime_delegate.h index 189d764b1eb5b..20059827b8150 100644 --- a/runtime/runtime_delegate.h +++ b/runtime/runtime_delegate.h @@ -37,6 +37,10 @@ class RuntimeDelegate { virtual void SetNeedsReportTimings(bool value) = 0; + virtual std::unique_ptr> + ComputePlatformResolvedLocale( + const std::vector& supported_locale_data) = 0; + protected: virtual ~RuntimeDelegate(); }; diff --git a/shell/common/engine.cc b/shell/common/engine.cc index 9f03c9200fd32..a5d8a8050d0b9 100644 --- a/shell/common/engine.cc +++ b/shell/common/engine.cc @@ -492,6 +492,11 @@ void Engine::UpdateIsolateDescription(const std::string isolate_name, delegate_.UpdateIsolateDescription(isolate_name, isolate_port); } +std::unique_ptr> Engine::ComputePlatformResolvedLocale( + const std::vector& supported_locale_data) { + return delegate_.ComputePlatformResolvedLocale(supported_locale_data); +} + void Engine::SetNeedsReportTimings(bool needs_reporting) { delegate_.SetNeedsReportTimings(needs_reporting); } diff --git a/shell/common/engine.h b/shell/common/engine.h index ede9b0426488f..14dbed2fd570f 100644 --- a/shell/common/engine.h +++ b/shell/common/engine.h @@ -227,6 +227,25 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { /// collected and send back to Dart. /// virtual void SetNeedsReportTimings(bool needs_reporting) = 0; + + //-------------------------------------------------------------------------- + /// @brief Directly invokes platform-specific APIs to compute the + /// locale the platform would have natively resolved to. + /// + /// @param[in] supported_locale_data The vector of strings that represents + /// the locales supported by the app. + /// Each locale consists of three + /// strings: languageCode, countryCode, + /// and scriptCode in that order. + /// + /// @return A vector of 3 strings languageCode, countryCode, and + /// scriptCode that represents the locale selected by the + /// platform. Empty strings mean the value was unassigned. Empty + /// vector represents a null locale. + /// + virtual std::unique_ptr> + ComputePlatformResolvedLocale( + const std::vector& supported_locale_data) = 0; }; //---------------------------------------------------------------------------- @@ -765,6 +784,10 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { void UpdateIsolateDescription(const std::string isolate_name, int64_t isolate_port) override; + // |RuntimeDelegate| + std::unique_ptr> ComputePlatformResolvedLocale( + const std::vector& supported_locale_data) override; + void SetNeedsReportTimings(bool value) override; void StopAnimator(); diff --git a/shell/common/platform_view.cc b/shell/common/platform_view.cc index 9fa42ae17d22e..146874933e739 100644 --- a/shell/common/platform_view.cc +++ b/shell/common/platform_view.cc @@ -137,4 +137,12 @@ void PlatformView::SetNextFrameCallback(const fml::closure& closure) { delegate_.OnPlatformViewSetNextFrameCallback(closure); } +std::unique_ptr> +PlatformView::ComputePlatformResolvedLocales( + const std::vector& supported_locale_data) { + std::unique_ptr> out = + std::make_unique>(); + return out; +} + } // namespace flutter diff --git a/shell/common/platform_view.h b/shell/common/platform_view.h index 4e226dab30901..5596a01e59230 100644 --- a/shell/common/platform_view.h +++ b/shell/common/platform_view.h @@ -209,6 +209,25 @@ class PlatformView { /// virtual void OnPlatformViewMarkTextureFrameAvailable( int64_t texture_id) = 0; + + //-------------------------------------------------------------------------- + /// @brief Directly invokes platform-specific APIs to compute the + /// locale the platform would have natively resolved to. + /// + /// @param[in] supported_locale_data The vector of strings that represents + /// the locales supported by the app. + /// Each locale consists of three + /// strings: languageCode, countryCode, + /// and scriptCode in that order. + /// + /// @return A vector of 3 strings languageCode, countryCode, and + /// scriptCode that represents the locale selected by the + /// platform. Empty strings mean the value was unassigned. Empty + /// vector represents a null locale. + /// + virtual std::unique_ptr> + ComputePlatformViewResolvedLocale( + const std::vector& supported_locale_data) = 0; }; //---------------------------------------------------------------------------- @@ -543,6 +562,25 @@ class PlatformView { /// void MarkTextureFrameAvailable(int64_t texture_id); + //-------------------------------------------------------------------------- + /// @brief Directly invokes platform-specific APIs to compute the + /// locale the platform would have natively resolved to. + /// + /// @param[in] supported_locale_data The vector of strings that represents + /// the locales supported by the app. + /// Each locale consists of three + /// strings: languageCode, countryCode, + /// and scriptCode in that order. + /// + /// @return A vector of 3 strings languageCode, countryCode, and + /// scriptCode that represents the locale selected by the + /// platform. Empty strings mean the value was unassigned. Empty + /// vector represents a null locale. + /// + virtual std::unique_ptr> + ComputePlatformResolvedLocales( + const std::vector& supported_locale_data); + protected: PlatformView::Delegate& delegate_; const TaskRunners task_runners_; diff --git a/shell/common/shell.cc b/shell/common/shell.cc index 2becbe493dd64..a99c8209d0eaf 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -1097,6 +1097,19 @@ void Shell::SetNeedsReportTimings(bool value) { needs_report_timings_ = value; } +// |Engine::Delegate| +std::unique_ptr> Shell::ComputePlatformResolvedLocale( + const std::vector& supported_locale_data) { + return ComputePlatformViewResolvedLocale(supported_locale_data); +} + +// |PlatformView::Delegate| +std::unique_ptr> +Shell::ComputePlatformViewResolvedLocale( + const std::vector& supported_locale_data) { + return platform_view_->ComputePlatformResolvedLocales(supported_locale_data); +} + void Shell::ReportTimings() { FML_DCHECK(is_setup_); FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); diff --git a/shell/common/shell.h b/shell/common/shell.h index 1484104e9397e..e41e7143e9929 100644 --- a/shell/common/shell.h +++ b/shell/common/shell.h @@ -483,6 +483,10 @@ class Shell final : public PlatformView::Delegate, // |PlatformView::Delegate| void OnPlatformViewSetNextFrameCallback(const fml::closure& closure) override; + // |PlatformView::Delegate| + std::unique_ptr> ComputePlatformViewResolvedLocale( + const std::vector& supported_locale_data) override; + // |Animator::Delegate| void OnAnimatorBeginFrame(fml::TimePoint frame_target_time) override; @@ -517,6 +521,10 @@ class Shell final : public PlatformView::Delegate, // |Engine::Delegate| void SetNeedsReportTimings(bool value) override; + // |Engine::Delegate| + std::unique_ptr> ComputePlatformResolvedLocale( + const std::vector& supported_locale_data) override; + // |Rasterizer::Delegate| void OnFrameRasterized(const FrameTiming&) override; diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 94ee1f77d8a33..fc33b1ce4b474 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -210,6 +210,7 @@ android_java_sources = [ "io/flutter/plugin/editing/FlutterTextUtils.java", "io/flutter/plugin/editing/InputConnectionAdaptor.java", "io/flutter/plugin/editing/TextInputPlugin.java", + "io/flutter/plugin/localization/LocalizationPlugin.java", "io/flutter/plugin/mouse/MouseCursorPlugin.java", "io/flutter/plugin/platform/AccessibilityEventsDelegate.java", "io/flutter/plugin/platform/PlatformPlugin.java", diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index a2091e2c2b492..c05b37bdb4e67 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -11,7 +11,6 @@ import android.graphics.Insets; import android.graphics.Rect; import android.os.Build; -import android.os.LocaleList; import android.text.format.DateFormat; import android.util.AttributeSet; import android.util.SparseArray; @@ -40,14 +39,11 @@ import io.flutter.embedding.engine.renderer.RenderSurface; import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.plugin.editing.TextInputPlugin; +import io.flutter.plugin.localization.LocalizationPlugin; import io.flutter.plugin.mouse.MouseCursorPlugin; 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; import java.util.Set; /** @@ -101,6 +97,7 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC // existing, stateless system channels, e.g., KeyEventChannel, TextInputChannel, etc. @Nullable private MouseCursorPlugin mouseCursorPlugin; @Nullable private TextInputPlugin textInputPlugin; + @Nullable private LocalizationPlugin localizationPlugin; @Nullable private AndroidKeyProcessor androidKeyProcessor; @Nullable private AndroidTouchProcessor androidTouchProcessor; @Nullable private AccessibilityBridge accessibilityBridge; @@ -354,7 +351,7 @@ protected void onConfigurationChanged(@NonNull Configuration newConfig) { // again (e.g. in onStart). if (flutterEngine != null) { Log.v(TAG, "Configuration changed. Sending locales and user settings to Flutter."); - sendLocalesToFlutter(newConfig); + localizationPlugin.sendLocalesToFlutter(newConfig); sendUserSettingsToFlutter(); } } @@ -815,6 +812,7 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) { this, this.flutterEngine.getTextInputChannel(), this.flutterEngine.getPlatformViewsController()); + localizationPlugin = this.flutterEngine.getLocalizationPlugin(); androidKeyProcessor = new AndroidKeyProcessor(this.flutterEngine.getKeyEventChannel(), textInputPlugin); androidTouchProcessor = new AndroidTouchProcessor(this.flutterEngine.getRenderer()); @@ -841,7 +839,7 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) { // Push View and Context related information from Android to Flutter. sendUserSettingsToFlutter(); - sendLocalesToFlutter(getResources().getConfiguration()); + localizationPlugin.sendLocalesToFlutter(getResources().getConfiguration()); sendViewportMetricsToFlutter(); flutterEngine.getPlatformViewsController().attachToView(this); @@ -944,42 +942,6 @@ public void removeFlutterEngineAttachmentListener( flutterEngineAttachmentListeners.remove(listener); } - /** - * Send the current {@link Locale} configuration to Flutter. - * - *

FlutterEngine must be non-null when this method is invoked. - */ - @SuppressWarnings("deprecation") - private void sendLocalesToFlutter(@NonNull Configuration config) { - List locales = new ArrayList<>(); - if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { - LocaleList localeList = config.getLocales(); - int localeCount = localeList.size(); - for (int index = 0; index < localeCount; ++index) { - Locale locale = localeList.get(index); - locales.add(locale); - } - } else { - locales.add(config.locale); - } - - Locale platformResolvedLocale = null; - if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - List languageRanges = new ArrayList<>(); - 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); - } - /** * Send various user preferences of this Android device to Flutter. * diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 053e9b92a1eb9..1408156ebaf24 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -28,6 +28,7 @@ import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.embedding.engine.systemchannels.SystemChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel; +import io.flutter.plugin.localization.LocalizationPlugin; import io.flutter.plugin.platform.PlatformViewsController; import java.lang.reflect.Method; import java.util.HashSet; @@ -73,6 +74,7 @@ public class FlutterEngine { @NonNull private final FlutterRenderer renderer; @NonNull private final DartExecutor dartExecutor; @NonNull private final FlutterEnginePluginRegistry pluginRegistry; + @NonNull private final LocalizationPlugin localizationPlugin; // System channels. @NonNull private final AccessibilityChannel accessibilityChannel; @@ -260,21 +262,9 @@ public FlutterEngine( @Nullable String[] dartVmArgs, boolean automaticallyRegisterPlugins, boolean waitForRestorationData) { - this.flutterJNI = flutterJNI; - flutterLoader.startInitialization(context.getApplicationContext()); - flutterLoader.ensureInitializationComplete(context, dartVmArgs); - - flutterJNI.addEngineLifecycleListener(engineLifecycleListener); - flutterJNI.setPlatformViewsController(platformViewsController); - attachToJni(); - this.dartExecutor = new DartExecutor(flutterJNI, context.getAssets()); this.dartExecutor.onAttachedToJNI(); - // TODO(mattcarroll): FlutterRenderer is temporally coupled to attach(). Remove that coupling if - // possible. - this.renderer = new FlutterRenderer(flutterJNI); - accessibilityChannel = new AccessibilityChannel(dartExecutor, flutterJNI); keyEventChannel = new KeyEventChannel(dartExecutor); lifecycleChannel = new LifecycleChannel(dartExecutor); @@ -287,6 +277,21 @@ public FlutterEngine( systemChannel = new SystemChannel(dartExecutor); textInputChannel = new TextInputChannel(dartExecutor); + this.localizationPlugin = new LocalizationPlugin(context, localizationChannel); + + this.flutterJNI = flutterJNI; + flutterLoader.startInitialization(context.getApplicationContext()); + flutterLoader.ensureInitializationComplete(context, dartVmArgs); + + flutterJNI.addEngineLifecycleListener(engineLifecycleListener); + flutterJNI.setPlatformViewsController(platformViewsController); + flutterJNI.setLocalizationPlugin(localizationPlugin); + attachToJni(); + + // TODO(mattcarroll): FlutterRenderer is temporally coupled to attach(). Remove that coupling if + // possible. + this.renderer = new FlutterRenderer(flutterJNI); + this.platformViewsController = platformViewsController; this.platformViewsController.onAttachedToJNI(); @@ -486,6 +491,12 @@ public PluginRegistry getPlugins() { return pluginRegistry; } + /** The LocalizationPlugin this FlutterEngine created. */ + @NonNull + public LocalizationPlugin getLocalizationPlugin() { + return localizationPlugin; + } + /** * {@code PlatformViewsController}, which controls all platform views running within this {@code * FlutterEngine}. diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index 3f6155f93f0b1..5fb4e337c6ef5 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -8,6 +8,7 @@ import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.SurfaceTexture; +import android.os.Build; import android.os.Looper; import android.view.Surface; import android.view.SurfaceHolder; @@ -22,10 +23,14 @@ import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener; import io.flutter.embedding.engine.renderer.RenderSurface; import io.flutter.plugin.common.StandardMessageCodec; +import io.flutter.plugin.localization.LocalizationPlugin; import io.flutter.plugin.platform.PlatformViewsController; import io.flutter.view.AccessibilityBridge; import io.flutter.view.FlutterCallbackInformation; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; @@ -169,6 +174,7 @@ public static native void nativeOnVsync( @Nullable private Long nativePlatformViewId; @Nullable private AccessibilityDelegate accessibilityDelegate; @Nullable private PlatformMessageHandler platformMessageHandler; + @Nullable private LocalizationPlugin localizationPlugin; @Nullable private PlatformViewsController platformViewsController; @NonNull @@ -834,6 +840,65 @@ public FlutterOverlaySurface createOverlaySurface() { } // ----- End Engine Lifecycle Support ---- + // ----- Start Localization Support ---- + + /** Sets the localization plugin that is used in various localization methods. */ + @UiThread + public void setLocalizationPlugin(@Nullable LocalizationPlugin localizationPlugin) { + ensureRunningOnMainThread(); + this.localizationPlugin = localizationPlugin; + } + + /** Invoked by native to obtain the results of Android's locale resolution algorithm. */ + @SuppressWarnings("unused") + @VisibleForTesting + String[] computePlatformResolvedLocale(@NonNull String[] strings) { + if (localizationPlugin == null) { + return new String[0]; + } + List supportedLocales = new ArrayList(); + final int localeDataLength = 3; + for (int i = 0; i < strings.length; i += localeDataLength) { + String languageCode = strings[i + 0]; + String countryCode = strings[i + 1]; + String scriptCode = strings[i + 2]; + // Convert to Locales via LocaleBuilder if available (API 24+) to include scriptCode. + if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + Locale.Builder localeBuilder = new Locale.Builder(); + if (!languageCode.isEmpty()) { + localeBuilder.setLanguage(languageCode); + } + if (!countryCode.isEmpty()) { + localeBuilder.setRegion(countryCode); + } + if (!scriptCode.isEmpty()) { + localeBuilder.setScript(scriptCode); + } + supportedLocales.add(localeBuilder.build()); + } else { + // Pre-API 24, we fall back on scriptCode-less locales. + supportedLocales.add(new Locale(languageCode, countryCode)); + } + } + + Locale result = localizationPlugin.resolveNativeLocale(supportedLocales); + + if (result == null) { + return new String[0]; + } + String[] output = new String[localeDataLength]; + output[0] = result.getLanguage(); + output[1] = result.getCountry(); + if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + output[2] = result.getScript(); + } else { + output[2] = ""; + } + return output; + } + + // ----- End Localization Support ---- + // @SuppressWarnings("unused") @UiThread public void onDisplayPlatformView(int viewId, int x, int y, int width, int height) { 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 580b2df8764d5..0f3ef7f0cf1d0 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java @@ -26,20 +26,8 @@ public LocalizationChannel(@NonNull DartExecutor dartExecutor) { } /** Send the given {@code locales} to Dart. */ - public void sendLocales(@NonNull List locales, Locale platformResolvedLocale) { + public void sendLocales(@NonNull List locales) { 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) { @@ -60,4 +48,20 @@ public void sendLocales(@NonNull List locales, Locale platformResolvedLo } channel.invokeMethod("setLocale", data); } + + /** Send the given {@code platformResolvedLocale} to Dart. */ + public void sendPlatformResolvedLocales(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); + } + } } diff --git a/shell/platform/android/io/flutter/plugin/localization/LocalizationPlugin.java b/shell/platform/android/io/flutter/plugin/localization/LocalizationPlugin.java new file mode 100644 index 0000000000000..ed75dbb2541d5 --- /dev/null +++ b/shell/platform/android/io/flutter/plugin/localization/LocalizationPlugin.java @@ -0,0 +1,71 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugin.localization; + +import android.content.Context; +import android.content.res.Configuration; +import android.os.Build; +import android.os.LocaleList; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.LocalizationChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** Android implementation of the localization plugin. */ +public class LocalizationPlugin { + @NonNull private final LocalizationChannel localizationChannel; + @NonNull private final Context context; + + public LocalizationPlugin( + @NonNull Context context, @NonNull LocalizationChannel localizationChannel) { + + this.context = context; + this.localizationChannel = localizationChannel; + } + + public Locale resolveNativeLocale(List supportedLocales) { + Locale platformResolvedLocale = null; + if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + List languageRanges = new ArrayList<>(); + LocaleList localeList = context.getResources().getConfiguration().getLocales(); + int localeCount = localeList.size(); + for (int index = 0; index < localeCount; ++index) { + Locale locale = localeList.get(index); + String localeString = locale.toString(); + // This string replacement converts the locale string into the ranges format. + languageRanges.add(new Locale.LanguageRange(localeString.replace("_", "-"))); + } + + // TODO(garyq): This should be modified to achieve Android's full + // locale resolution: + // https://developer.android.com/guide/topics/resources/multilingual-support + platformResolvedLocale = Locale.lookup(languageRanges, supportedLocales); + } + return platformResolvedLocale; + } + + /** + * Send the current {@link Locale} configuration to Flutter. + * + *

FlutterEngine must be non-null when this method is invoked. + */ + @SuppressWarnings("deprecation") + public void sendLocalesToFlutter(@NonNull Configuration config) { + List locales = new ArrayList<>(); + if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + LocaleList localeList = config.getLocales(); + int localeCount = localeList.size(); + for (int index = 0; index < localeCount; ++index) { + Locale locale = localeList.get(index); + locales.add(locale); + } + } else { + locales.add(config.locale); + } + + localizationChannel.sendLocales(locales); + } +} diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index e01f975303ed5..cd0446b95d446 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -17,7 +17,6 @@ import android.graphics.SurfaceTexture; import android.os.Build; import android.os.Handler; -import android.os.LocaleList; import android.text.format.DateFormat; import android.util.AttributeSet; import android.util.Log; @@ -59,14 +58,13 @@ import io.flutter.plugin.common.ActivityLifecycleListener; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.editing.TextInputPlugin; +import io.flutter.plugin.localization.LocalizationPlugin; import io.flutter.plugin.mouse.MouseCursorPlugin; import io.flutter.plugin.platform.PlatformPlugin; 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; /** @@ -124,6 +122,7 @@ static final class ViewportMetrics { private final SystemChannel systemChannel; private final InputMethodManager mImm; private final TextInputPlugin mTextInputPlugin; + private final LocalizationPlugin mLocalizationPlugin; private final MouseCursorPlugin mMouseCursorPlugin; private final AndroidKeyProcessor androidKeyProcessor; private final AndroidTouchProcessor androidTouchProcessor; @@ -231,15 +230,17 @@ public void onPostResume() { } else { mMouseCursorPlugin = null; } + mLocalizationPlugin = new LocalizationPlugin(context, localizationChannel); androidKeyProcessor = new AndroidKeyProcessor(keyEventChannel, mTextInputPlugin); androidTouchProcessor = new AndroidTouchProcessor(flutterRenderer); mNativeView .getPluginRegistry() .getPlatformViewsController() .attachTextInputPlugin(mTextInputPlugin); + mNativeView.getFlutterJNI().setLocalizationPlugin(mLocalizationPlugin); // Send initial platform information to Dart - sendLocalesToDart(getResources().getConfiguration()); + mLocalizationPlugin.sendLocalesToFlutter(getResources().getConfiguration()); sendUserPlatformSettingsToDart(); } @@ -404,41 +405,10 @@ private void sendUserPlatformSettingsToDart() { .send(); } - @SuppressWarnings("deprecation") - private void sendLocalesToDart(Configuration config) { - List locales = new ArrayList<>(); - if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { - LocaleList localeList = config.getLocales(); - int localeCount = localeList.size(); - for (int index = 0; index < localeCount; ++index) { - Locale locale = localeList.get(index); - locales.add(locale); - } - } else { - locales.add(config.locale); - } - - Locale platformResolvedLocale = null; - if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - List languageRanges = new ArrayList<>(); - 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 protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - sendLocalesToDart(newConfig); + mLocalizationPlugin.sendLocalesToFlutter(newConfig); sendUserPlatformSettingsToDart(); } diff --git a/shell/platform/android/jni/platform_view_android_jni.h b/shell/platform/android/jni/platform_view_android_jni.h index efbca69e5112f..51f11a1db7b16 100644 --- a/shell/platform/android/jni/platform_view_android_jni.h +++ b/shell/platform/android/jni/platform_view_android_jni.h @@ -153,6 +153,13 @@ class PlatformViewAndroidJNI { /// @note Must be called from the platform thread. /// virtual void FlutterViewCreateOverlaySurface() = 0; + + //---------------------------------------------------------------------------- + /// @brief Computes the locale Android would select. + /// + virtual std::unique_ptr> + FlutterViewComputePlatformResolvedLocale( + std::vector supported_locales_data) = 0; }; } // namespace flutter diff --git a/shell/platform/android/platform_view_android.cc b/shell/platform/android/platform_view_android.cc index 0cae52e370c4d..311028dce920f 100644 --- a/shell/platform/android/platform_view_android.cc +++ b/shell/platform/android/platform_view_android.cc @@ -395,6 +395,14 @@ void PlatformViewAndroid::ReleaseResourceContext() const { } } +// |PlatformView| +std::unique_ptr> +PlatformViewAndroid::ComputePlatformResolvedLocales( + const std::vector& supported_locale_data) { + return jni_facade_->FlutterViewComputePlatformResolvedLocale( + supported_locale_data); +} + void PlatformViewAndroid::InstallFirstFrameCallback() { // On Platform Task Runner. SetNextFrameCallback( diff --git a/shell/platform/android/platform_view_android.h b/shell/platform/android/platform_view_android.h index 9f859a1dcdaf5..2a3474e8d2194 100644 --- a/shell/platform/android/platform_view_android.h +++ b/shell/platform/android/platform_view_android.h @@ -107,6 +107,10 @@ class PlatformViewAndroid final : public PlatformView { // |PlatformView| void ReleaseResourceContext() const override; + // |PlatformView| + std::unique_ptr> ComputePlatformResolvedLocales( + const std::vector& supported_locale_data) override; + void InstallFirstFrameCallback(); void FireFirstFrameCallback(); diff --git a/shell/platform/android/platform_view_android_jni_impl.cc b/shell/platform/android/platform_view_android_jni_impl.cc index c23f5b48ba742..34fb2736da774 100644 --- a/shell/platform/android/platform_view_android_jni_impl.cc +++ b/shell/platform/android/platform_view_android_jni_impl.cc @@ -95,6 +95,9 @@ static jmethodID g_get_transform_matrix_method = nullptr; static jmethodID g_detach_from_gl_context_method = nullptr; +static jmethodID g_compute_platform_resolved_locale_method = nullptr; + +// Called By Java static jmethodID g_on_display_platform_view_method = nullptr; static jmethodID g_on_display_overlay_surface_method = nullptr; @@ -804,6 +807,15 @@ bool PlatformViewAndroid::Register(JNIEnv* env) { return false; } + g_compute_platform_resolved_locale_method = env->GetMethodID( + g_flutter_jni_class->obj(), "computePlatformResolvedLocale", + "([Ljava/lang/String;)[Ljava/lang/String;"); + + if (g_compute_platform_resolved_locale_method == nullptr) { + FML_LOG(ERROR) << "Could not locate computePlatformResolvedLocale method"; + return false; + } + return RegisterApi(env); } @@ -1112,4 +1124,32 @@ void PlatformViewAndroidJNIImpl::FlutterViewCreateOverlaySurface() { FML_CHECK(CheckException(env)); } +std::unique_ptr> +PlatformViewAndroidJNIImpl::FlutterViewComputePlatformResolvedLocale( + std::vector supported_locales_data) { + JNIEnv* env = fml::jni::AttachCurrentThread(); + + std::unique_ptr> out = + std::make_unique>(); + + auto java_object = java_object_.get(env); + if (java_object.is_null()) { + return out; + } + fml::jni::ScopedJavaLocalRef j_locales_data = + fml::jni::VectorToStringArray(env, supported_locales_data); + jobjectArray result = (jobjectArray)env->CallObjectMethod( + java_object.obj(), g_compute_platform_resolved_locale_method, + j_locales_data.obj()); + + FML_CHECK(CheckException(env)); + + int length = env->GetArrayLength(result); + for (int i = 0; i < length; i++) { + out->emplace_back(fml::jni::JavaStringToString( + env, (jstring)env->GetObjectArrayElement(result, i))); + } + return out; +} + } // namespace flutter diff --git a/shell/platform/android/platform_view_android_jni_impl.h b/shell/platform/android/platform_view_android_jni_impl.h index 25b62987ebf18..b6fa849006d40 100644 --- a/shell/platform/android/platform_view_android_jni_impl.h +++ b/shell/platform/android/platform_view_android_jni_impl.h @@ -68,6 +68,10 @@ class PlatformViewAndroidJNIImpl final : public PlatformViewAndroidJNI { void FlutterViewCreateOverlaySurface() override; + std::unique_ptr> + FlutterViewComputePlatformResolvedLocale( + std::vector supported_locales_data) override; + private: // Reference to FlutterJNI object. const fml::jni::JavaObjectWeakGlobalRef java_object_; diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java index 5c6198f8983be..2c8e243375fd7 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -32,6 +32,7 @@ import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.embedding.engine.systemchannels.SystemChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel; +import io.flutter.plugin.localization.LocalizationPlugin; import io.flutter.plugin.platform.PlatformViewsController; import org.junit.Before; import org.junit.Test; @@ -626,6 +627,7 @@ private FlutterEngine mockFlutterEngine() { when(engine.getTextInputChannel()).thenReturn(mock(TextInputChannel.class)); when(engine.getMouseCursorChannel()).thenReturn(mock(MouseCursorChannel.class)); when(engine.getActivityControlSurface()).thenReturn(mock(ActivityControlSurface.class)); + when(engine.getLocalizationPlugin()).thenReturn(mock(LocalizationPlugin.class)); return engine; } diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java index cf0fdcc589808..7461674f243b0 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java @@ -102,14 +102,13 @@ public void onConfigurationChanged_fizzlesWhenNullEngine() { Configuration configuration = RuntimeEnvironment.application.getResources().getConfiguration(); // 1 invocation of channels. flutterView.attachToFlutterEngine(flutterEngine); - // 2 invocations of channels. flutterView.onConfigurationChanged(configuration); flutterView.detachFromFlutterEngine(); // Should fizzle. flutterView.onConfigurationChanged(configuration); - verify(flutterEngine, times(2)).getLocalizationChannel(); + verify(flutterEngine, times(1)).getLocalizationPlugin(); verify(flutterEngine, times(2)).getSettingsChannel(); } diff --git a/shell/platform/android/test/io/flutter/embedding/engine/FlutterJNITest.java b/shell/platform/android/test/io/flutter/embedding/engine/FlutterJNITest.java index f63530a555ac7..43c52675db895 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/FlutterJNITest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/FlutterJNITest.java @@ -4,9 +4,19 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; - +import static org.mockito.Mockito.when; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.LocaleList; +import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener; +import io.flutter.embedding.engine.systemchannels.LocalizationChannel; +import io.flutter.plugin.localization.LocalizationPlugin; import io.flutter.plugin.platform.PlatformViewsController; +import java.util.Locale; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; import org.junit.runner.RunWith; @@ -15,6 +25,7 @@ @Config(manifest = Config.NONE) @RunWith(RobolectricTestRunner.class) +@TargetApi(24) // LocaleList and scriptCode are API 24+. public class FlutterJNITest { @Test public void itAllowsFirstFrameListenersToRemoveThemselvesInline() { @@ -50,6 +61,85 @@ public void onFlutterUiNoLongerDisplayed() {} } @Test + public void computePlatformResolvedLocaleCallsLocalizationPluginProperly() { + // --- Test Setup --- + FlutterJNI flutterJNI = new FlutterJNI(); + + Context context = mock(Context.class); + Resources resources = mock(Resources.class); + Configuration config = mock(Configuration.class); + DartExecutor dartExecutor = mock(DartExecutor.class); + LocaleList localeList = + new LocaleList(new Locale("es", "MX"), new Locale("zh", "CN"), new Locale("en", "US")); + when(context.getResources()).thenReturn(resources); + when(resources.getConfiguration()).thenReturn(config); + when(config.getLocales()).thenReturn(localeList); + + flutterJNI.setLocalizationPlugin( + new LocalizationPlugin(context, new LocalizationChannel(dartExecutor))); + String[] supportedLocales = + new String[] { + "fr", "FR", "", + "zh", "", "", + "en", "CA", "" + }; + String[] result = flutterJNI.computePlatformResolvedLocale(supportedLocales); + assertEquals(result.length, 3); + assertEquals(result[0], "zh"); + assertEquals(result[1], ""); + assertEquals(result[2], ""); + + supportedLocales = + new String[] { + "fr", "FR", "", + "ar", "", "", + "en", "CA", "" + }; + result = flutterJNI.computePlatformResolvedLocale(supportedLocales); + assertEquals(result.length, 0); // This should change when full algo is implemented. + + supportedLocales = + new String[] { + "fr", "FR", "", + "ar", "", "", + "en", "US", "" + }; + result = flutterJNI.computePlatformResolvedLocale(supportedLocales); + assertEquals(result.length, 3); + assertEquals(result[0], "en"); + assertEquals(result[1], "US"); + assertEquals(result[2], ""); + + supportedLocales = + new String[] { + "ar", "", "", + "es", "MX", "", + "en", "US", "" + }; + result = flutterJNI.computePlatformResolvedLocale(supportedLocales); + assertEquals(result.length, 3); + assertEquals(result[0], "es"); + assertEquals(result[1], "MX"); + assertEquals(result[2], ""); + + // Empty supportedLocales. + supportedLocales = new String[] {}; + result = flutterJNI.computePlatformResolvedLocale(supportedLocales); + assertEquals(result.length, 0); + + // Empty preferredLocales. + supportedLocales = + new String[] { + "fr", "FR", "", + "zh", "", "", + "en", "CA", "" + }; + localeList = new LocaleList(); + when(config.getLocales()).thenReturn(localeList); + result = flutterJNI.computePlatformResolvedLocale(supportedLocales); + assertEquals(result.length, 0); + } + public void onDisplayPlatformView__callsPlatformViewsController() { PlatformViewsController platformViewsController = mock(PlatformViewsController.class); diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm index f293be0895786..2cf8b74538ed5 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm @@ -85,6 +85,12 @@ void OnPlatformViewSetAccessibilityFeatures(int32_t flags) override {} void OnPlatformViewRegisterTexture(std::shared_ptr texture) override {} void OnPlatformViewUnregisterTexture(int64_t texture_id) override {} void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) override {} + + std::unique_ptr> ComputePlatformViewResolvedLocale( + const std::vector& supported_locale_data) override { + std::unique_ptr> out = std::make_unique>(); + return out; + } }; class MockIosDelegate : public AccessibilityBridge::IosDelegate { diff --git a/shell/platform/darwin/ios/platform_view_ios.h b/shell/platform/darwin/ios/platform_view_ios.h index a89192d908ca5..7cd3f3468e627 100644 --- a/shell/platform/darwin/ios/platform_view_ios.h +++ b/shell/platform/darwin/ios/platform_view_ios.h @@ -129,6 +129,7 @@ class PlatformViewIOS final : public PlatformView { fml::scoped_nsprotocol text_input_plugin_; fml::closure firstFrameCallback_; ScopedObserver dealloc_view_controller_observer_; + std::vector platform_resolved_locale_; // |PlatformView| void HandlePlatformMessage(fml::RefPtr message) override; @@ -152,6 +153,10 @@ class PlatformViewIOS final : public PlatformView { // |PlatformView| void OnPreEngineRestart() const override; + // |PlatformView| + std::unique_ptr> ComputePlatformResolvedLocales( + const std::vector& supported_locale_data) override; + FML_DISALLOW_COPY_AND_ASSIGN(PlatformViewIOS); }; diff --git a/shell/platform/darwin/ios/platform_view_ios.mm b/shell/platform/darwin/ios/platform_view_ios.mm index d2f4cb48a26bd..14fcc2141c478 100644 --- a/shell/platform/darwin/ios/platform_view_ios.mm +++ b/shell/platform/darwin/ios/platform_view_ios.mm @@ -191,6 +191,39 @@ new AccessibilityBridge(static_cast(owner_controller_.get().view), [owner_controller_.get() platformViewsController]->Reset(); } +std::unique_ptr> PlatformViewIOS::ComputePlatformResolvedLocales( + const std::vector& supported_locale_data) { + size_t localeDataLength = 3; + NSMutableArray* supported_locale_identifiers = + [NSMutableArray arrayWithCapacity:supported_locale_data.size() / localeDataLength]; + for (size_t i = 0; i < supported_locale_data.size(); i += localeDataLength) { + NSDictionary* dict = @{ + NSLocaleLanguageCode : [NSString stringWithUTF8String:supported_locale_data[i].c_str()], + NSLocaleCountryCode : [NSString stringWithUTF8String:supported_locale_data[i + 1].c_str()], + NSLocaleScriptCode : [NSString stringWithUTF8String:supported_locale_data[i + 2].c_str()] + }; + [supported_locale_identifiers addObject:[NSLocale localeIdentifierFromComponents:dict]]; + } + NSArray* result = + [NSBundle preferredLocalizationsFromArray:supported_locale_identifiers]; + + // Output format should be either empty or 3 strings for language, country, and script. + std::unique_ptr> out = std::make_unique>(); + + if (result != nullptr && [result count] > 0) { + if (@available(ios 10.0, *)) { + NSLocale* locale = [NSLocale localeWithLocaleIdentifier:[result firstObject]]; + NSString* languageCode = [locale languageCode]; + out->emplace_back(languageCode == nullptr ? "" : languageCode.UTF8String); + NSString* countryCode = [locale countryCode]; + out->emplace_back(countryCode == nullptr ? "" : countryCode.UTF8String); + NSString* scriptCode = [locale scriptCode]; + out->emplace_back(scriptCode == nullptr ? "" : scriptCode.UTF8String); + } + } + return out; +} + PlatformViewIOS::ScopedObserver::ScopedObserver() : observer_(nil) {} PlatformViewIOS::ScopedObserver::~ScopedObserver() { diff --git a/shell/platform/embedder/platform_view_embedder.cc b/shell/platform/embedder/platform_view_embedder.cc index b6daf1946704a..b1a2cac536e54 100644 --- a/shell/platform/embedder/platform_view_embedder.cc +++ b/shell/platform/embedder/platform_view_embedder.cc @@ -93,4 +93,13 @@ std::unique_ptr PlatformViewEmbedder::CreateVSyncWaiter() { platform_dispatch_table_.vsync_callback, task_runners_); } +// |PlatformView| +std::unique_ptr> +PlatformViewEmbedder::ComputePlatformResolvedLocales( + const std::vector& supported_locale_data) { + std::unique_ptr> out = + std::make_unique>(); + return out; +} + } // namespace flutter diff --git a/shell/platform/embedder/platform_view_embedder.h b/shell/platform/embedder/platform_view_embedder.h index 13d4c45684655..9815d94936918 100644 --- a/shell/platform/embedder/platform_view_embedder.h +++ b/shell/platform/embedder/platform_view_embedder.h @@ -76,6 +76,10 @@ class PlatformViewEmbedder final : public PlatformView { // |PlatformView| std::unique_ptr CreateVSyncWaiter() override; + // |PlatformView| + std::unique_ptr> ComputePlatformResolvedLocales( + const std::vector& supported_locale_data) override; + FML_DISALLOW_COPY_AND_ASSIGN(PlatformViewEmbedder); }; diff --git a/shell/platform/fuchsia/flutter/platform_view_unittest.cc b/shell/platform/fuchsia/flutter/platform_view_unittest.cc index 077a8996f93f4..6d6b30df8f1f6 100644 --- a/shell/platform/fuchsia/flutter/platform_view_unittest.cc +++ b/shell/platform/fuchsia/flutter/platform_view_unittest.cc @@ -74,6 +74,13 @@ class MockPlatformViewDelegate : public flutter::PlatformView::Delegate { void OnPlatformViewUnregisterTexture(int64_t texture_id) {} // |flutter::PlatformView::Delegate| void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) {} + // |flutter::PlatformView::Delegate| + std::unique_ptr> ComputePlatformViewResolvedLocale( + const std::vector& supported_locale_data) { + std::unique_ptr> out = + std::make_unique>(); + return out; + } bool SemanticsEnabled() const { return semantics_enabled_; } int32_t SemanticsFeatures() const { return semantics_features_; } diff --git a/testing/dart/window_test.dart b/testing/dart/window_test.dart index 714df2d0a25bb..a434482024d69 100644 --- a/testing/dart/window_test.dart +++ b/testing/dart/window_test.dart @@ -25,4 +25,16 @@ void main() { final FrameTiming timing = FrameTiming([1000, 8000, 9000, 19500]); expect(timing.toString(), 'FrameTiming(buildDuration: 7.0ms, rasterDuration: 10.5ms, totalSpan: 18.5ms)'); }); + + test('computePlatformResolvedLocale basic', () { + final List supportedLocales = [ + const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'), + const Locale.fromSubtags(languageCode: 'fr', countryCode: 'FR'), + const Locale.fromSubtags(languageCode: 'en', countryCode: 'US'), + const Locale.fromSubtags(languageCode: 'en'), + ]; + // The default implementation returns null due to lack of a real platform. + final Locale result = window.computePlatformResolvedLocale(supportedLocales); + expect(result, null); + }); }