// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/device_posture/device_posture_registry_watcher_win.h"
#include <optional>
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_map.h"
#include "base/json/json_reader.h"
#include "base/strings/string_split.h"
#include "base/strings/utf_string_conversions.h"
using blink::mojom::DevicePostureType;
namespace {
// The full specification of the registry is located over here
// https://github.com/foldable-devices/foldable-windows-registry-specification
// This approach is a stop gap solution until Windows gains proper APIs.
//
// TODO(crbug.com/40276180): When Windows gains the APIs we should update this
// code.
//
// FOLED stands for Foldable OLED.
constexpr wchar_t kFoledRegKeyPath[] = L"Software\\Intel\\Foled";
// On Windows the platform returns [left][fold][right] and so far we support
// only one display feature in Chromium.
constexpr int kFirstFoldInSegmentsArray = 1;
} // namespace
namespace content {
// static
DevicePostureRegistryWatcherWin*
DevicePostureRegistryWatcherWin::GetInstance() {
static base::NoDestructor<DevicePostureRegistryWatcherWin> instance;
return instance.get();
}
DevicePostureRegistryWatcherWin::DevicePostureRegistryWatcherWin() {
base::win::RegKey registry_key(HKEY_CURRENT_USER, kFoledRegKeyPath,
KEY_QUERY_VALUE);
if (registry_key.Valid()) {
ComputeFoldableState(registry_key, /*notify_changes=*/false);
}
}
DevicePostureRegistryWatcherWin::~DevicePostureRegistryWatcherWin() = default;
void DevicePostureRegistryWatcherWin::AddObserver(
DevicePosturePlatformProviderWin* observer) {
if (!observer) {
return;
}
DCHECK(!observers_.HasObserver(observer));
if (observers_.empty()) {
DCHECK(!registry_key_);
registry_key_.emplace(HKEY_CURRENT_USER, kFoledRegKeyPath,
KEY_NOTIFY | KEY_QUERY_VALUE);
if (registry_key_->Valid()) {
// Start watching the registry for changes.
registry_key_->StartWatching(
base::BindOnce(&DevicePostureRegistryWatcherWin::OnRegistryKeyChanged,
base::Unretained(this)));
}
}
observers_.AddObserver(observer);
// Inform the observer with the current state.
observer->UpdateDevicePosture(current_posture_);
observer->UpdateDisplayFeatureBounds(current_display_feature_bounds_);
}
void DevicePostureRegistryWatcherWin::RemoveObserver(
DevicePosturePlatformProviderWin* observer) {
observers_.RemoveObserver(observer);
if (observers_.empty()) {
registry_key_ = std::nullopt;
}
}
std::optional<DevicePostureType> DevicePostureRegistryWatcherWin::ParsePosture(
std::string_view posture_state) {
static constexpr auto kPostureStateToPostureType =
base::MakeFixedFlatMap<std::string_view, DevicePostureType>(
{{"MODE_HANDHELD", DevicePostureType::kFolded},
{"MODE_DUAL_ANGLE", DevicePostureType::kFolded},
{"MODE_LAPTOP_KB", DevicePostureType::kContinuous},
{"MODE_LAYFLAT_LANDSCAPE", DevicePostureType::kContinuous},
{"MODE_LAYFLAT_PORTRAIT", DevicePostureType::kContinuous},
{"MODE_TABLETOP", DevicePostureType::kContinuous}});
if (auto iter = kPostureStateToPostureType.find(posture_state);
iter != kPostureStateToPostureType.end()) {
return iter->second;
}
DVLOG(1) << "Could not parse the posture data: " << posture_state;
return std::nullopt;
}
void DevicePostureRegistryWatcherWin::ComputeFoldableState(
const base::win::RegKey& registry_key,
bool notify_changes) {
CHECK(registry_key.Valid());
std::wstring posture_data;
if (registry_key.ReadValue(L"PostureData", &posture_data) != ERROR_SUCCESS) {
return;
}
std::optional<base::Value::Dict> dict =
base::JSONReader::ReadDict(base::WideToUTF8(posture_data));
if (!dict) {
DVLOG(1) << "Could not read the foldable status.";
return;
}
const std::string* posture_state = dict->FindString("PostureState");
if (!posture_state) {
return;
}
const DevicePostureType old_posture = current_posture_;
std::optional<DevicePostureType> posture = ParsePosture(*posture_state);
if (posture) {
current_posture_ = posture.value();
if (old_posture != current_posture_ && notify_changes) {
for (DevicePosturePlatformProviderWin& observer : observers_) {
observer.UpdateDevicePosture(current_posture_);
}
}
}
base::Value::List* viewport_segments = dict->FindList("Rectangles");
if (!viewport_segments) {
DVLOG(1) << "Could not parse the viewport segments data.";
return;
}
std::optional<std::vector<gfx::Rect>> segments =
ParseViewportSegments(*viewport_segments);
if (!segments) {
return;
}
// If there is not enough segments then the display feature is empty.
if (segments->size() < 2) {
current_display_feature_bounds_ = gfx::Rect();
} else {
// We want the first fold segment of the segment array.
current_display_feature_bounds_ = segments->at(kFirstFoldInSegmentsArray);
}
if (notify_changes) {
for (DevicePosturePlatformProviderWin& observer : observers_) {
observer.UpdateDisplayFeatureBounds(current_display_feature_bounds_);
}
}
}
std::optional<std::vector<gfx::Rect>>
DevicePostureRegistryWatcherWin::ParseViewportSegments(
const base::Value::List& viewport_segments) {
if (viewport_segments.empty()) {
return std::nullopt;
}
// Check if the list is correctly constructed. It should be a multiple of
// |left side|fold|right side| or 1.
if (viewport_segments.size() != 1 && viewport_segments.size() % 3 != 0) {
DVLOG(1) << "Could not parse the viewport segments data.";
return std::nullopt;
}
std::vector<gfx::Rect> segments;
for (const auto& segment : viewport_segments) {
const std::string* segment_string = segment.GetIfString();
if (!segment_string) {
DVLOG(1) << "Could not parse the viewport segments data";
return std::nullopt;
}
auto rectangle_dimensions = base::SplitStringPiece(
*segment_string, ",", base::WhitespaceHandling::TRIM_WHITESPACE,
base::SplitResult::SPLIT_WANT_NONEMPTY);
if (rectangle_dimensions.size() != 4) {
DVLOG(1) << "Could not parse the viewport segments data: "
<< *segment_string;
return std::nullopt;
}
int x, y, width, height;
if (!base::StringToInt(rectangle_dimensions[0], &x) ||
!base::StringToInt(rectangle_dimensions[1], &y) ||
!base::StringToInt(rectangle_dimensions[2], &width) ||
!base::StringToInt(rectangle_dimensions[3], &height)) {
DVLOG(1) << "Could not parse the viewport segments data: "
<< *segment_string;
return std::nullopt;
}
segments.emplace_back(x, y, width, height);
}
return segments;
}
void DevicePostureRegistryWatcherWin::OnRegistryKeyChanged() {
// |OnRegistryKeyChanged| is removed as an observer when the ChangeCallback is
// called, so we need to re-register.
registry_key_->StartWatching(
base::BindOnce(&DevicePostureRegistryWatcherWin::OnRegistryKeyChanged,
base::Unretained(this)));
ComputeFoldableState(registry_key_.value(), /*notify_changes=*/true);
}
} // namespace content