Platform resolved locale and Android localization refactor (flutter/engine#18645)

This commit is contained in:
Gary Qian 2020-06-16 04:29:50 -07:00 committed by GitHub
parent 082179c37b
commit c9fa545cf3
36 changed files with 598 additions and 106 deletions

View File

@ -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

View File

@ -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<Locale> supportedLocales) {
final List<String?> supportedLocalesData = <String?>[];
for (Locale locale in supportedLocales) {
supportedLocalesData.add(locale.languageCode);
supportedLocalesData.add(locale.countryCode);
supportedLocalesData.add(locale.scriptCode);
}
final List<String> 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<String> _computePlatformResolvedLocale(List<String?> 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

View File

@ -425,6 +425,27 @@ void Window::CompletePlatformMessageResponse(int response_id,
response->Complete(std::make_unique<fml::DataMapping>(std::move(data)));
}
Dart_Handle ComputePlatformResolvedLocale(Dart_Handle supportedLocalesHandle) {
std::vector<std::string> supportedLocales =
tonic::DartConverter<std::vector<std::string>>::FromDart(
supportedLocalesHandle);
std::vector<std::string> results =
*UIDartState::Current()
->window()
->client()
->ComputePlatformResolvedLocale(supportedLocales);
return tonic::DartConverter<std::vector<std::string>>::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},
});
}

View File

@ -59,6 +59,9 @@ class WindowClient {
int64_t isolate_port) = 0;
virtual void SetNeedsReportTimings(bool value) = 0;
virtual std::shared_ptr<const fml::Mapping> GetPersistentIsolateData() = 0;
virtual std::unique_ptr<std::vector<std::string>>
ComputePlatformResolvedLocale(
const std::vector<std::string>& supported_locale_data) = 0;
protected:
virtual ~WindowClient();

View File

@ -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<Locale> 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

View File

@ -340,6 +340,13 @@ RuntimeController::GetPersistentIsolateData() {
return persistent_isolate_data_;
}
// |WindowClient|
std::unique_ptr<std::vector<std::string>>
RuntimeController::ComputePlatformResolvedLocale(
const std::vector<std::string>& supported_locale_data) {
return client_.ComputePlatformResolvedLocale(supported_locale_data);
}
Dart_Port RuntimeController::GetMainPort() {
std::shared_ptr<DartIsolate> root_isolate = root_isolate_.lock();
return root_isolate ? root_isolate->main_port() : ILLEGAL_PORT;

View File

@ -522,6 +522,10 @@ class RuntimeController final : public WindowClient {
// |WindowClient|
std::shared_ptr<const fml::Mapping> GetPersistentIsolateData() override;
// |WindowClient|
std::unique_ptr<std::vector<std::string>> ComputePlatformResolvedLocale(
const std::vector<std::string>& supported_locale_data) override;
FML_DISALLOW_COPY_AND_ASSIGN(RuntimeController);
};

View File

@ -37,6 +37,10 @@ class RuntimeDelegate {
virtual void SetNeedsReportTimings(bool value) = 0;
virtual std::unique_ptr<std::vector<std::string>>
ComputePlatformResolvedLocale(
const std::vector<std::string>& supported_locale_data) = 0;
protected:
virtual ~RuntimeDelegate();
};

View File

@ -492,6 +492,11 @@ void Engine::UpdateIsolateDescription(const std::string isolate_name,
delegate_.UpdateIsolateDescription(isolate_name, isolate_port);
}
std::unique_ptr<std::vector<std::string>> Engine::ComputePlatformResolvedLocale(
const std::vector<std::string>& supported_locale_data) {
return delegate_.ComputePlatformResolvedLocale(supported_locale_data);
}
void Engine::SetNeedsReportTimings(bool needs_reporting) {
delegate_.SetNeedsReportTimings(needs_reporting);
}

View File

@ -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<std::vector<std::string>>
ComputePlatformResolvedLocale(
const std::vector<std::string>& 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<std::vector<std::string>> ComputePlatformResolvedLocale(
const std::vector<std::string>& supported_locale_data) override;
void SetNeedsReportTimings(bool value) override;
void StopAnimator();

View File

@ -137,4 +137,12 @@ void PlatformView::SetNextFrameCallback(const fml::closure& closure) {
delegate_.OnPlatformViewSetNextFrameCallback(closure);
}
std::unique_ptr<std::vector<std::string>>
PlatformView::ComputePlatformResolvedLocales(
const std::vector<std::string>& supported_locale_data) {
std::unique_ptr<std::vector<std::string>> out =
std::make_unique<std::vector<std::string>>();
return out;
}
} // namespace flutter

View File

@ -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<std::vector<std::string>>
ComputePlatformViewResolvedLocale(
const std::vector<std::string>& 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<std::vector<std::string>>
ComputePlatformResolvedLocales(
const std::vector<std::string>& supported_locale_data);
protected:
PlatformView::Delegate& delegate_;
const TaskRunners task_runners_;

View File

@ -1097,6 +1097,19 @@ void Shell::SetNeedsReportTimings(bool value) {
needs_report_timings_ = value;
}
// |Engine::Delegate|
std::unique_ptr<std::vector<std::string>> Shell::ComputePlatformResolvedLocale(
const std::vector<std::string>& supported_locale_data) {
return ComputePlatformViewResolvedLocale(supported_locale_data);
}
// |PlatformView::Delegate|
std::unique_ptr<std::vector<std::string>>
Shell::ComputePlatformViewResolvedLocale(
const std::vector<std::string>& supported_locale_data) {
return platform_view_->ComputePlatformResolvedLocales(supported_locale_data);
}
void Shell::ReportTimings() {
FML_DCHECK(is_setup_);
FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread());

View File

@ -483,6 +483,10 @@ class Shell final : public PlatformView::Delegate,
// |PlatformView::Delegate|
void OnPlatformViewSetNextFrameCallback(const fml::closure& closure) override;
// |PlatformView::Delegate|
std::unique_ptr<std::vector<std::string>> ComputePlatformViewResolvedLocale(
const std::vector<std::string>& 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<std::vector<std::string>> ComputePlatformResolvedLocale(
const std::vector<std::string>& supported_locale_data) override;
// |Rasterizer::Delegate|
void OnFrameRasterized(const FrameTiming&) override;

View File

@ -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",

View File

@ -11,7 +11,6 @@ import android.content.res.Configuration;
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.FlutterUiDisplayListener;
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 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
// 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 class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
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 class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
// 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 class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
flutterEngineAttachmentListeners.remove(listener);
}
/**
* Send the current {@link Locale} configuration to Flutter.
*
* <p>FlutterEngine must be non-null when this method is invoked.
*/
@SuppressWarnings("deprecation")
private void sendLocalesToFlutter(@NonNull Configuration config) {
List<Locale> 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<Locale.LanguageRange> 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.
*

View File

@ -28,6 +28,7 @@ import io.flutter.embedding.engine.systemchannels.RestorationChannel;
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 class 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 class 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 class FlutterEngine {
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}.

View File

@ -8,6 +8,7 @@ import android.content.Context;
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.dart.PlatformMessageHandler;
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 class FlutterJNI {
@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 class FlutterJNI {
}
// ----- 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<Locale> supportedLocales = new ArrayList<Locale>();
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) {

View File

@ -26,20 +26,8 @@ public class LocalizationChannel {
}
/** Send the given {@code locales} to Dart. */
public void sendLocales(@NonNull List<Locale> locales, Locale platformResolvedLocale) {
public void sendLocales(@NonNull List<Locale> 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<String> 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<String> data = new ArrayList<>();
for (Locale locale : locales) {
@ -60,4 +48,20 @@ public class LocalizationChannel {
}
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<String> 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);
}
}
}

View File

@ -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<Locale> supportedLocales) {
Locale platformResolvedLocale = null;
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
List<Locale.LanguageRange> 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.
*
* <p>FlutterEngine must be non-null when this method is invoked.
*/
@SuppressWarnings("deprecation")
public void sendLocalesToFlutter(@NonNull Configuration config) {
List<Locale> 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);
}
}

View File

@ -17,7 +17,6 @@ import android.graphics.Rect;
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.embedding.engine.systemchannels.TextInputChannel;
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 @@ public class FlutterView extends SurfaceView
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 class FlutterView extends SurfaceView
} 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 @@ public class FlutterView extends SurfaceView
.send();
}
@SuppressWarnings("deprecation")
private void sendLocalesToDart(Configuration config) {
List<Locale> 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<Locale.LanguageRange> 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();
}

View File

@ -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<std::vector<std::string>>
FlutterViewComputePlatformResolvedLocale(
std::vector<std::string> supported_locales_data) = 0;
};
} // namespace flutter

View File

@ -395,6 +395,14 @@ void PlatformViewAndroid::ReleaseResourceContext() const {
}
}
// |PlatformView|
std::unique_ptr<std::vector<std::string>>
PlatformViewAndroid::ComputePlatformResolvedLocales(
const std::vector<std::string>& supported_locale_data) {
return jni_facade_->FlutterViewComputePlatformResolvedLocale(
supported_locale_data);
}
void PlatformViewAndroid::InstallFirstFrameCallback() {
// On Platform Task Runner.
SetNextFrameCallback(

View File

@ -107,6 +107,10 @@ class PlatformViewAndroid final : public PlatformView {
// |PlatformView|
void ReleaseResourceContext() const override;
// |PlatformView|
std::unique_ptr<std::vector<std::string>> ComputePlatformResolvedLocales(
const std::vector<std::string>& supported_locale_data) override;
void InstallFirstFrameCallback();
void FireFirstFrameCallback();

View File

@ -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<std::vector<std::string>>
PlatformViewAndroidJNIImpl::FlutterViewComputePlatformResolvedLocale(
std::vector<std::string> supported_locales_data) {
JNIEnv* env = fml::jni::AttachCurrentThread();
std::unique_ptr<std::vector<std::string>> out =
std::make_unique<std::vector<std::string>>();
auto java_object = java_object_.get(env);
if (java_object.is_null()) {
return out;
}
fml::jni::ScopedJavaLocalRef<jobjectArray> 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

View File

@ -68,6 +68,10 @@ class PlatformViewAndroidJNIImpl final : public PlatformViewAndroidJNI {
void FlutterViewCreateOverlaySurface() override;
std::unique_ptr<std::vector<std::string>>
FlutterViewComputePlatformResolvedLocale(
std::vector<std::string> supported_locales_data) override;
private:
// Reference to FlutterJNI object.
const fml::jni::JavaObjectWeakGlobalRef java_object_;

View File

@ -32,6 +32,7 @@ import io.flutter.embedding.engine.systemchannels.NavigationChannel;
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 @@ public class FlutterActivityAndFragmentDelegateTest {
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;
}

View File

@ -102,14 +102,13 @@ public class FlutterViewTest {
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();
}

View File

@ -4,9 +4,19 @@ import static org.junit.Assert.assertEquals;
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 @@ import org.robolectric.annotation.Config;
@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 class FlutterJNITest {
}
@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);

View File

@ -85,6 +85,12 @@ class MockDelegate : public PlatformView::Delegate {
void OnPlatformViewRegisterTexture(std::shared_ptr<Texture> texture) override {}
void OnPlatformViewUnregisterTexture(int64_t texture_id) override {}
void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) override {}
std::unique_ptr<std::vector<std::string>> ComputePlatformViewResolvedLocale(
const std::vector<std::string>& supported_locale_data) override {
std::unique_ptr<std::vector<std::string>> out = std::make_unique<std::vector<std::string>>();
return out;
}
};
class MockIosDelegate : public AccessibilityBridge::IosDelegate {

View File

@ -129,6 +129,7 @@ class PlatformViewIOS final : public PlatformView {
fml::scoped_nsprotocol<FlutterTextInputPlugin*> text_input_plugin_;
fml::closure firstFrameCallback_;
ScopedObserver dealloc_view_controller_observer_;
std::vector<std::string> platform_resolved_locale_;
// |PlatformView|
void HandlePlatformMessage(fml::RefPtr<flutter::PlatformMessage> message) override;
@ -152,6 +153,10 @@ class PlatformViewIOS final : public PlatformView {
// |PlatformView|
void OnPreEngineRestart() const override;
// |PlatformView|
std::unique_ptr<std::vector<std::string>> ComputePlatformResolvedLocales(
const std::vector<std::string>& supported_locale_data) override;
FML_DISALLOW_COPY_AND_ASSIGN(PlatformViewIOS);
};

View File

@ -191,6 +191,39 @@ void PlatformViewIOS::OnPreEngineRestart() const {
[owner_controller_.get() platformViewsController]->Reset();
}
std::unique_ptr<std::vector<std::string>> PlatformViewIOS::ComputePlatformResolvedLocales(
const std::vector<std::string>& supported_locale_data) {
size_t localeDataLength = 3;
NSMutableArray<NSString*>* supported_locale_identifiers =
[NSMutableArray arrayWithCapacity:supported_locale_data.size() / localeDataLength];
for (size_t i = 0; i < supported_locale_data.size(); i += localeDataLength) {
NSDictionary<NSString*, NSString*>* 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<NSString*>* result =
[NSBundle preferredLocalizationsFromArray:supported_locale_identifiers];
// Output format should be either empty or 3 strings for language, country, and script.
std::unique_ptr<std::vector<std::string>> out = std::make_unique<std::vector<std::string>>();
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() {

View File

@ -93,4 +93,13 @@ std::unique_ptr<VsyncWaiter> PlatformViewEmbedder::CreateVSyncWaiter() {
platform_dispatch_table_.vsync_callback, task_runners_);
}
// |PlatformView|
std::unique_ptr<std::vector<std::string>>
PlatformViewEmbedder::ComputePlatformResolvedLocales(
const std::vector<std::string>& supported_locale_data) {
std::unique_ptr<std::vector<std::string>> out =
std::make_unique<std::vector<std::string>>();
return out;
}
} // namespace flutter

View File

@ -76,6 +76,10 @@ class PlatformViewEmbedder final : public PlatformView {
// |PlatformView|
std::unique_ptr<VsyncWaiter> CreateVSyncWaiter() override;
// |PlatformView|
std::unique_ptr<std::vector<std::string>> ComputePlatformResolvedLocales(
const std::vector<std::string>& supported_locale_data) override;
FML_DISALLOW_COPY_AND_ASSIGN(PlatformViewEmbedder);
};

View File

@ -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<std::vector<std::string>> ComputePlatformViewResolvedLocale(
const std::vector<std::string>& supported_locale_data) {
std::unique_ptr<std::vector<std::string>> out =
std::make_unique<std::vector<std::string>>();
return out;
}
bool SemanticsEnabled() const { return semantics_enabled_; }
int32_t SemanticsFeatures() const { return semantics_features_; }

View File

@ -25,4 +25,16 @@ void main() {
final FrameTiming timing = FrameTiming(<int>[1000, 8000, 9000, 19500]);
expect(timing.toString(), 'FrameTiming(buildDuration: 7.0ms, rasterDuration: 10.5ms, totalSpan: 18.5ms)');
});
test('computePlatformResolvedLocale basic', () {
final List<Locale> supportedLocales = <Locale>[
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);
});
}