Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Get user's preferred languages from registry#36536

Merged
yaakovschectman merged 26 commits intoflutter:mainfrom
yaakovschectman:get_user_locales
Oct 6, 2022
Merged

Get user's preferred languages from registry#36536
yaakovschectman merged 26 commits intoflutter:mainfrom
yaakovschectman:get_user_locales

Conversation

@yaakovschectman
Copy link
Contributor

The function currently used in GetPreferredLanguages on Windows does not necessarily return all the languages included in the list of preferred languages set in the user's settings. By attempting to get these values from the registry first, we can obtain all these languages in order. These are eventually parsed and used as the PlatformConfiguration's locales.

flutter/flutter#112717

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide and the C++, Objective-C, Java style guides.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. See testing the engine for instructions on
    writing and running engine tests.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the CLA.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Copy link
Member

@cbracken cbracken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make this code testable now that we're touching it.

If we haven't already, it might be worth writing a small Registry class that wraps the registry calls, so we can use a fake in the tests.

We keep hitting the issue of Win32 C API calls in tests. It might be worth biting the bullet and creating a Win32API struct with trampoline functions for these like we do with the embedder API in the engine. @loic-sharma may have thoughts on that.

&buffer_size) != S_OK) {
return languages;
}
for (int i = 0; i < buffer_size; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Returns a UTF-16 encoded wide string equivalent of a UTF-8 encoded input
// string.
std::wstring Utf8ToWideString(const std::string_view str);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

str_buffer contains a multi-string delimited by nulls, so I believe we would need a for loop here no matter what. We could call this function on each substring of the multi-string and increment the counter by its length instead of iterating over each character, but I do not see any benefit to this.

}

// Get the list of null-separated languages.
// Mutli-string must be at least 3-long if non-empty,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Mutli-string must be at least 3-long if non-empty,
// Multi-string must be at least 3-long if non-empty,

// Determine where languages are defined and get buffer length
if (RegGetValueA(HKEY_CURRENT_USER,
"Control panel\\International\\User Profile", "Languages",
RRF_RT_REG_MULTI_SZ, NULL, NULL, &buffer_size) != S_OK) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

S_OK is technically correct but ERROR_SUCCESS would be more idiomatic: https://learn.microsoft.com/windows/win32/api/winreg/nf-winreg-reggetvaluea#return-value

// Mutli-string must be at least 3-long if non-empty,
// as a multi-string is terminated with 2 nulls.
//
// See: https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-value-types
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Microsoft docs are localized only if you remove the language code:

Suggested change
// See: https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-value-types
// See: https://learn.microsoft.com/windows/win32/sysinfo/registry-value-types

if (!::GetThreadPreferredUILanguages(flags, &count, nullptr, &buffer_size)) {
return languages;
// Determine where languages are defined and get buffer length
if (RegGetValueA(HKEY_CURRENT_USER,
Copy link
Member

@loic-sharma loic-sharma Oct 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is using RegGetValueA instead of RegGetValue intentional? If no, prefer RegGetValue. That might have the added benefit of removing the str_buffer to buffer conversion below.

@loic-sharma
Copy link
Member

We keep hitting the issue of Win32 C API calls in tests. It might be worth biting the bullet and creating a Win32API struct with trampoline functions for these like we do with the embedder API in the engine.

That sounds great to me! What are y'alls' thoughts on using windows_proc_table as that trampoline? It was originally meant to provide fallbacks for older Windows versions, but I think the name is general enough that it could be expanded to all win32 APIs for mocking purposes.

@yaakovschectman
Copy link
Contributor Author

That sounds great to me! What are y'alls' thoughts on using windows_proc_table as that trampoline? It was originally meant to provide fallbacks for older Windows versions, but I think the name is general enough that it could be expanded to all win32 APIs for mocking purposes.

WindowsProcTable is a member of Window, we would need a member of FlutterEngineWindows to provide the registry functionality. I am unsure if having a reference to the proc table in both the window and the engine is desirable. I am thinking of having a registry API class as a member of the engine at the moment.

LPDWORD sizeData) const {
static const wchar_t* locales =
L"en-US\0zh-Hans-CN\0ja\0zh-Hant-TW\0he\0\0";
static DWORD locales_len = 35;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way to get the length of locales programmatically?

https://stackoverflow.com/questions/2853615/get-length-of-wchar-t-in-c

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue here is that it's not just a string, it's a multistring, which means it contains null delimiters in the middle that string length methods will treat as a terminator and return a bad value for. We could write our own method that counts until it reaches a double null, though I don't know how much use it would get outside of this test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the incoming data a string of locales delimited by \0?

Copy link
Contributor Author

@yaakovschectman yaakovschectman Oct 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Each locale is delimited by \0 with a double \0 at the end.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol, I wonder why they would do that 🤷‍♂️ . Anyway, the change LGTM now 👍 .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's how the Windows registry stores all multi strings, or at least how it represents them when you request the data.

Copy link
Member

@cbracken cbracken Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to do this as follows:

using namespace std::string_literals;
static const std::wstring locales = L"en-US\0zh-Hans-CN\0ja\0zh-Hant-TW\0he\0\0"s;
static const DWORD locales_len = locales.size();

Copy link
Member

@cbracken cbracken Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That said -- the documentation for the pcbData param to GetRegValue function states this value should be in bytes, so we should really return:

static const DWORD locales_len = locales.size() * sizeof(wchar_t);

which in this case works out to 140.

Looks like you're already doing that multiplication below.

return languages;
// Determine where languages are defined and get buffer length
if (registry.GetRegistryValue(HKEY_CURRENT_USER,
L"Control panel\\International\\User Profile",
Copy link
Contributor

@a-wallen a-wallen Oct 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like these strings gets referenced twice

L"Control panel\International\User Profile",
L"Languages"

Should we make these constants? It might help improve readability 🤷‍♂️

Copy link
Contributor

@a-wallen a-wallen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for following my feedback! This change LGTM, but since @cbracken has pending changes, he should give the final approval.

// The on frame drawn callback.
fml::closure next_frame_callback_;

// A handle to a registry for registry values
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// A handle to a registry for registry values
// A handle to a registry for registry values.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even briefer, and a bit more correct, since technically this is a pointer and not a handle (which, while being a type of void* has a very specific meaning in Windows):

Suggested change
// A handle to a registry for registry values
// Wrapper providing Windows registry access.


#include "flutter/shell/platform/windows/flutter_windows_engine.h"

#include <string>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the changes in this file necessary or can they be reverted?

Comment on lines +10 to +11
#include <string>
#include <vector>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these includes necessary?

Suggested change
#include <string>
#include <vector>

void UpdateAccessibilityFeatures(FlutterAccessibilityFeature flags);

// Allow setting the Windows Registry
void SetWindowsRegistry(const WindowsRegistry& windows_registry);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used? If no, please removing it as it's dead code. If yes, should this be a std::unique_ptr that's moved by the caller to avoid a copy?

PVOID data,
LPDWORD sizeData) const;

private:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private:
private:
FML_DISALLOW_COPY_AND_ASSIGN(WindowsRegistry);

@skia-gold
Copy link

Gold has detected about 52 new digest(s) on patchset 16.
View them at https://flutter-engine-gold.skia.org/cl/github/36536

@skia-gold
Copy link

Gold has detected about 2 new digest(s) on patchset 16.
View them at https://flutter-engine-gold.skia.org/cl/github/36536

LPDWORD type,
PVOID data,
LPDWORD dataSize) const {
return RegGetValue(hkey, key, value, flags, type, data, dataSize);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return RegGetValue(hkey, key, value, flags, type, data, dataSize);
return RegGetValue(hkey, key, value, flags, type, data, data_size);

DWORD flags,
LPDWORD type,
PVOID data,
LPDWORD sizeData) const;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
LPDWORD sizeData) const;
LPDWORD data_size) const;


class WindowsRegistry {
public:
explicit WindowsRegistry();
Copy link
Member

@cbracken cbracken Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explicit isn't needed here since there's no parameter to be eligible for implicit conversion (and because copy-initialization is already prevented by the disallow copy & assign macro, which already prevents something like WindowsRegistry r = {}).

Suggested change
explicit WindowsRegistry();
WindowsRegistry() = default;

class WindowsRegistry {
public:
explicit WindowsRegistry();
virtual ~WindowsRegistry();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
virtual ~WindowsRegistry();
virtual ~WindowsRegistry() = default;


namespace flutter {

WindowsRegistry::WindowsRegistry() {}
Copy link
Member

@cbracken cbracken Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can delete this if you apply the default suggestion below. Sadly I'm not GitHub-suggestion-savvy enough to know how to delete two lines in a suggested change :/

DWORD flags,
LPDWORD type,
PVOID data,
LPDWORD sizeData) const {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
LPDWORD sizeData) const {
LPDWORD data_size) const {

L"en-US\0zh-Hans-CN\0ja\0zh-Hant-TW\0he\0\0";
static DWORD locales_len = 35;
if (data != nullptr) {
if (*sizeData < locales_len) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (*sizeData < locales_len) {
if (*data_size < locales_len) {

if (*sizeData < locales_len) {
return ERROR_MORE_DATA;
}
memcpy(data, locales, locales_len * sizeof(wchar_t));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefix as std::memcpy and make sure to #include <cstring>, where it's defined.

memcpy(data, locales, locales_len * sizeof(wchar_t));
*sizeData = locales_len * sizeof(wchar_t);
} else if (sizeData != NULL) {
*sizeData = locales_len * sizeof(wchar_t);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same name fix here, but I have no idea how to do multiline suggestions in github :(

: project_(std::make_unique<FlutterProjectBundle>(project)),
aot_data_(nullptr, nullptr) {
aot_data_(nullptr, nullptr),
windows_registry_(std::make_unique<WindowsRegistry>()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In unit tests, I think you'll want to inject the registry, so you'll want to add a std::unique_ptr<WindowsRegistry> registry param, then move the unique the the construction site rather than in the initialiser list.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was my rationale for including the SetWindowsRegistry method originally. Can parameters have default arguments that are not constant expressions, or would we need to include this new argument anywhere a FlutterWindowsEngine is constructed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd need to include this wherever it's constructed, though other than unit tests, I suspect that should be very rare, possibly just one place. An alternative would be to add a second constructor that takes the registry and use that one in unit tests where it matters.

Since semantically, this isn't something we ever expect to mutate on an existing engine, I think constructor injection is preferrable.

Copy link
Member

@cbracken cbracken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, looks good barring mostly style-guide-ish nits. Thanks so much for factoring this out. This is great!

@HansMuller
Copy link

CC @thkim1011 - this PR is probably of interest

Copy link
Member

@cbracken cbracken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a quick run through the formatter, but other than that, looks great!

LGTM stamp from a Japanese personal seal

Copy link
Member

@loic-sharma loic-sharma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, nice work!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

6 participants