| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/display/display_prefs.h" |
| |
| #include <stdint.h> |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "ash/display/display_configuration_observer.h" |
| #include "ash/display/display_util.h" |
| #include "ash/display/resolution_notification_controller.h" |
| #include "ash/display/screen_orientation_controller.h" |
| #include "ash/display/window_tree_host_manager.h" |
| #include "ash/public/cpp/ash_pref_names.h" |
| #include "ash/session/test_session_controller_client.h" |
| #include "ash/shell.h" |
| #include "ash/test/ash_test_base.h" |
| #include "ash/test/ash_test_helper.h" |
| #include "ash/wm/tablet_mode/tablet_mode_controller.h" |
| #include "base/command_line.h" |
| #include "base/macros.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/values.h" |
| #include "chromeos/constants/chromeos_switches.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "components/prefs/testing_pref_service.h" |
| #include "components/user_manager/user_type.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/display/display_layout_builder.h" |
| #include "ui/display/display_switches.h" |
| #include "ui/display/manager/display_configurator.h" |
| #include "ui/display/manager/display_layout_store.h" |
| #include "ui/display/manager/display_manager.h" |
| #include "ui/display/manager/display_manager_utilities.h" |
| #include "ui/display/manager/json_converter.h" |
| #include "ui/display/manager/test/touch_device_manager_test_api.h" |
| #include "ui/display/screen.h" |
| #include "ui/display/test/display_manager_test_api.h" |
| #include "ui/gfx/geometry/vector3d_f.h" |
| |
| namespace ash { |
| |
| namespace { |
| const char kPrimaryIdKey[] = "primary-id"; |
| const char kPositionKey[] = "position"; |
| const char kOffsetKey[] = "offset"; |
| const char kPlacementDisplayIdKey[] = "placement.display_id"; |
| const char kPlacementParentDisplayIdKey[] = "placement.parent_display_id"; |
| |
| // The mean acceleration due to gravity on Earth in m/s^2. |
| const float kMeanGravity = -9.80665f; |
| |
| bool IsRotationLocked() { |
| return ash::Shell::Get()->screen_orientation_controller()->rotation_locked(); |
| } |
| |
| bool CompareTouchAssociations( |
| const display::TouchDeviceManager::TouchAssociationMap& map_1, |
| const display::TouchDeviceManager::TouchAssociationMap& map_2) { |
| if (map_1.size() != map_2.size()) |
| return false; |
| // Each iterator instance |entry| is a pair of type |
| // std::pair<display::TouchDeviceIdentifier, |
| // display::TouchDeviceManager::AssociationInfoMap> |
| for (const auto& entry : map_1) { |
| if (!map_2.count(entry.first)) |
| return false; |
| |
| const auto& association_info_map_1 = entry.second; |
| const auto& association_info_map_2 = map_2.at(entry.first); |
| if (association_info_map_1.size() != association_info_map_2.size()) |
| return false; |
| |
| // Each iterator instance is a pair of type: |
| // std::pair<int64_t, display::TouchDeviceManager::TouchAssociationInfo> |
| for (const auto& info_1 : association_info_map_1) { |
| if (!association_info_map_2.count(info_1.first)) |
| return false; |
| |
| const auto& info_2 = association_info_map_2.at(info_1.first); |
| if (!(info_1.second.timestamp == info_2.timestamp && |
| info_1.second.calibration_data == info_2.calibration_data)) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| bool ComparePortAssociations( |
| const display::TouchDeviceManager::PortAssociationMap& map_1, |
| const display::TouchDeviceManager::PortAssociationMap& map_2) { |
| if (map_1.size() != map_2.size()) |
| return false; |
| auto it_1 = map_1.begin(); |
| auto it_2 = map_2.begin(); |
| while (it_1 != map_1.end()) { |
| if (it_1->first != it_2->first) |
| return false; |
| if (it_1->second != it_2->second) |
| return false; |
| it_1++; |
| it_2++; |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| class DisplayPrefsTest : public AshTestBase { |
| protected: |
| DisplayPrefsTest() {} |
| |
| ~DisplayPrefsTest() override {} |
| |
| void SetUp() override { |
| disable_provide_local_state(); |
| AshTestBase::SetUp(); |
| DisplayPrefs::RegisterLocalStatePrefs(local_state_.registry()); |
| display_prefs()->SetPrefServiceForTest(&local_state_); |
| observer_ = std::make_unique<DisplayConfigurationObserver>(); |
| observer_->OnDisplaysInitialized(); |
| } |
| |
| void TearDown() override { |
| observer_.reset(); |
| AshTestBase::TearDown(); |
| } |
| |
| void LoggedInAsUser() { SimulateUserLogin("user1@test.com"); } |
| |
| void LoggedInAsGuest() { SimulateGuestLogin(); } |
| |
| void LoadDisplayPreferences() { display_prefs()->LoadDisplayPreferences(); } |
| |
| // Do not use the implementation of display_prefs.cc directly to avoid |
| // notifying the update to the system. |
| void StoreDisplayLayoutPrefForList( |
| const display::DisplayIdList& list, |
| display::DisplayPlacement::Position position, |
| int offset, |
| int64_t primary_id) { |
| std::string name = display::DisplayIdListToString(list); |
| DictionaryPrefUpdate update(local_state(), prefs::kSecondaryDisplays); |
| display::DisplayLayout display_layout; |
| display_layout.placement_list.emplace_back(position, offset); |
| display_layout.primary_id = primary_id; |
| |
| DCHECK(!name.empty()); |
| |
| base::DictionaryValue* pref_data = update.Get(); |
| std::unique_ptr<base::Value> layout_value(new base::DictionaryValue()); |
| base::Value* value = nullptr; |
| if (pref_data->Get(name, &value) && value != nullptr) |
| layout_value.reset(value->DeepCopy()); |
| if (display::DisplayLayoutToJson(display_layout, layout_value.get())) |
| pref_data->Set(name, std::move(layout_value)); |
| } |
| |
| void StoreDisplayPropertyForList(const display::DisplayIdList& list, |
| const std::string& key, |
| std::unique_ptr<base::Value> value) { |
| std::string name = display::DisplayIdListToString(list); |
| |
| DictionaryPrefUpdate update(local_state(), prefs::kSecondaryDisplays); |
| base::DictionaryValue* pref_data = update.Get(); |
| |
| base::Value* layout_value = pref_data->FindKey(name); |
| if (layout_value) { |
| static_cast<base::DictionaryValue*>(layout_value) |
| ->Set(key, std::move(value)); |
| } else { |
| std::unique_ptr<base::DictionaryValue> layout_value( |
| new base::DictionaryValue()); |
| layout_value->SetBoolean(key, value != nullptr); |
| pref_data->Set(name, std::move(layout_value)); |
| } |
| } |
| |
| void StoreDisplayBoolPropertyForList(const display::DisplayIdList& list, |
| const std::string& key, |
| bool value) { |
| StoreDisplayPropertyForList(list, key, |
| std::make_unique<base::Value>(value)); |
| } |
| |
| void StoreDisplayLayoutPrefForList(const display::DisplayIdList& list, |
| display::DisplayPlacement::Position layout, |
| int offset) { |
| StoreDisplayLayoutPrefForList(list, layout, offset, list[0]); |
| } |
| |
| void StoreDisplayOverscan(int64_t id, const gfx::Insets& insets) { |
| DictionaryPrefUpdate update(local_state(), prefs::kDisplayProperties); |
| const std::string name = base::Int64ToString(id); |
| |
| base::DictionaryValue* pref_data = update.Get(); |
| auto insets_value = std::make_unique<base::DictionaryValue>(); |
| insets_value->SetInteger("insets_top", insets.top()); |
| insets_value->SetInteger("insets_left", insets.left()); |
| insets_value->SetInteger("insets_bottom", insets.bottom()); |
| insets_value->SetInteger("insets_right", insets.right()); |
| pref_data->Set(name, std::move(insets_value)); |
| } |
| |
| display::Display::Rotation GetRotation() { |
| return ash::Shell::Get() |
| ->display_manager() |
| ->GetDisplayInfo(display::Display::InternalDisplayId()) |
| .GetRotation(display::Display::RotationSource::ACCELEROMETER); |
| } |
| |
| void StoreExternalDisplayMirrorInfo( |
| const std::set<int64_t>& external_display_mirror_info) { |
| ListPrefUpdate update(local_state(), prefs::kExternalDisplayMirrorInfo); |
| base::ListValue* pref_data = update.Get(); |
| pref_data->Clear(); |
| for (const auto& id : external_display_mirror_info) |
| pref_data->GetList().emplace_back(base::Value(base::Int64ToString(id))); |
| } |
| |
| std::string GetRegisteredDisplayPlacementStr( |
| const display::DisplayIdList& list) { |
| return ash::Shell::Get() |
| ->display_manager() |
| ->layout_store() |
| ->GetRegisteredDisplayLayout(list) |
| .placement_list[0] |
| .ToString(); |
| } |
| |
| chromeos::DisplayPowerState GetRequestedPowerState() const { |
| return ash::Shell::Get()->display_configurator()->GetRequestedPowerState(); |
| } |
| PrefService* local_state() { return &local_state_; } |
| DisplayPrefs* display_prefs() { return ash::Shell::Get()->display_prefs(); } |
| |
| private: |
| std::unique_ptr<WindowTreeHostManager::Observer> observer_; |
| TestingPrefServiceSimple local_state_; |
| base::test::ScopedFeatureList scoped_feature_list_; |
| |
| DISALLOW_COPY_AND_ASSIGN(DisplayPrefsTest); |
| }; |
| |
| class DisplayPrefsTestGuest : public DisplayPrefsTest { |
| public: |
| DisplayPrefsTestGuest() { set_start_session(false); } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(DisplayPrefsTestGuest); |
| }; |
| |
| TEST_F(DisplayPrefsTest, ListedLayoutOverrides) { |
| UpdateDisplay("100x100,200x200"); |
| |
| display::DisplayIdList list = display_manager()->GetCurrentDisplayIdList(); |
| display::DisplayIdList dummy_list = |
| display::test::CreateDisplayIdList2(list[0], list[1] + 1); |
| ASSERT_NE(list[0], dummy_list[1]); |
| |
| StoreDisplayLayoutPrefForList(list, display::DisplayPlacement::TOP, 20); |
| StoreDisplayLayoutPrefForList(dummy_list, display::DisplayPlacement::LEFT, |
| 30); |
| display_prefs()->StoreDisplayPowerStateForTest( |
| chromeos::DISPLAY_POWER_INTERNAL_OFF_EXTERNAL_ON); |
| |
| base::CommandLine::ForCurrentProcess()->AppendSwitch( |
| chromeos::switches::kFirstExecAfterBoot); |
| LoadDisplayPreferences(); |
| |
| // requested_power_state_ should be chromeos::DISPLAY_POWER_ALL_ON at boot |
| const base::Optional<chromeos::DisplayPowerState> requested_power_state = |
| ash::Shell::Get() |
| ->display_configurator() |
| ->GetRequestedPowerStateForTest(); |
| ASSERT_NE(base::nullopt, requested_power_state); |
| EXPECT_EQ(chromeos::DISPLAY_POWER_ALL_ON, *requested_power_state); |
| // DisplayPowerState should be ignored at boot. |
| EXPECT_EQ(chromeos::DISPLAY_POWER_ALL_ON, GetRequestedPowerState()); |
| |
| Shell::Get()->display_manager()->UpdateDisplays(); |
| // Check if the layout settings are notified to the system properly. |
| // The new layout overrides old layout. |
| // Inverted one of for specified pair (id1, id2). Not used for the list |
| // (id1, dummy_id) since dummy_id is not connected right now. |
| EXPECT_EQ("id=2200000001, parent=2200000000, top, 20", |
| Shell::Get() |
| ->display_manager() |
| ->GetCurrentDisplayLayout() |
| .placement_list[0] |
| .ToString()); |
| EXPECT_EQ("id=2200000001, parent=2200000000, top, 20", |
| GetRegisteredDisplayPlacementStr(list)); |
| EXPECT_EQ("id=2200000002, parent=2200000000, left, 30", |
| GetRegisteredDisplayPlacementStr(dummy_list)); |
| } |
| |
| TEST_F(DisplayPrefsTest, BasicStores) { |
| ash::WindowTreeHostManager* window_tree_host_manager = |
| ash::Shell::Get()->window_tree_host_manager(); |
| int64_t id1 = display::Screen::GetScreen()->GetPrimaryDisplay().id(); |
| |
| // For each configuration change, we store mirror info only for external |
| // displays. So set internal display first before adding display. |
| display::test::ScopedSetInternalDisplayId set_internal(display_manager(), |
| id1); |
| UpdateDisplay("200x200*2, 400x300#400x400|300x200*1.25"); |
| int64_t id2 = display_manager()->GetSecondaryDisplay().id(); |
| int64_t dummy_id = id2 + 1; |
| ASSERT_NE(id1, dummy_id); |
| |
| LoggedInAsUser(); |
| |
| display_manager()->SetLayoutForCurrentDisplays( |
| display::test::CreateDisplayLayout(display_manager(), |
| display::DisplayPlacement::TOP, 10)); |
| const display::DisplayLayout& layout = |
| display_manager()->GetCurrentDisplayLayout(); |
| EXPECT_EQ(display::DisplayPlacement::TOP, layout.placement_list[0].position); |
| EXPECT_EQ(10, layout.placement_list[0].offset); |
| |
| display::DisplayLayoutBuilder dummy_layout_builder(id1); |
| dummy_layout_builder.SetSecondaryPlacement( |
| dummy_id, display::DisplayPlacement::LEFT, 20); |
| std::unique_ptr<display::DisplayLayout> dummy_layout( |
| dummy_layout_builder.Build()); |
| display::DisplayIdList list = |
| display::test::CreateDisplayIdList2(id1, dummy_id); |
| display_prefs()->StoreDisplayLayoutPrefForTest(list, *dummy_layout); |
| |
| // Can't switch to a display that does not exist. |
| window_tree_host_manager->SetPrimaryDisplayId(dummy_id); |
| EXPECT_NE(dummy_id, display::Screen::GetScreen()->GetPrimaryDisplay().id()); |
| |
| window_tree_host_manager->SetOverscanInsets(id1, gfx::Insets(10, 11, 12, 13)); |
| display_manager()->SetDisplayRotation(id1, display::Display::ROTATE_90, |
| display::Display::RotationSource::USER); |
| |
| constexpr float zoom_factor_1 = 1.f / 2.25f; |
| constexpr float zoom_factor_2 = 1.60f; |
| |
| display_manager()->UpdateZoomFactor(id1, zoom_factor_1); |
| display_manager()->UpdateZoomFactor(id2, zoom_factor_2); |
| |
| // Set touch calibration data for display |id2|. |
| uint32_t id_1 = 1234; |
| uint32_t port_1 = 5678; |
| const display::TouchDeviceIdentifier touch_device_identifier_1(id_1, port_1); |
| display::TouchCalibrationData::CalibrationPointPairQuad point_pair_quad_1 = { |
| {std::make_pair(gfx::Point(10, 10), gfx::Point(11, 12)), |
| std::make_pair(gfx::Point(190, 10), gfx::Point(195, 8)), |
| std::make_pair(gfx::Point(10, 90), gfx::Point(12, 94)), |
| std::make_pair(gfx::Point(190, 90), gfx::Point(189, 88))}}; |
| gfx::Size touch_size_1(200, 150); |
| |
| uint32_t id_2 = 2345; |
| uint32_t port_2 = 3456; |
| const display::TouchDeviceIdentifier touch_device_identifier_2(id_2, port_2); |
| display::TouchCalibrationData::CalibrationPointPairQuad point_pair_quad_2 = { |
| {std::make_pair(gfx::Point(10, 10), gfx::Point(11, 12)), |
| std::make_pair(gfx::Point(190, 10), gfx::Point(195, 8)), |
| std::make_pair(gfx::Point(10, 90), gfx::Point(12, 94)), |
| std::make_pair(gfx::Point(190, 90), gfx::Point(189, 88))}}; |
| gfx::Size touch_size_2(150, 150); |
| |
| // Create a 3rd touch device which has the same primary ID as the 2nd touch |
| // device but is connected to a different port. |
| uint32_t port_3 = 1357; |
| const display::TouchDeviceIdentifier touch_device_identifier_3(id_2, port_3); |
| |
| display_manager()->SetTouchCalibrationData( |
| id2, point_pair_quad_1, touch_size_1, touch_device_identifier_1); |
| display_manager()->SetTouchCalibrationData( |
| id2, point_pair_quad_2, touch_size_2, touch_device_identifier_2); |
| display_manager()->SetTouchCalibrationData( |
| id2, point_pair_quad_2, touch_size_1, touch_device_identifier_3); |
| |
| const base::DictionaryValue* displays = |
| local_state()->GetDictionary(prefs::kSecondaryDisplays); |
| const base::DictionaryValue* layout_value = nullptr; |
| std::string key = base::Int64ToString(id1) + "," + base::Int64ToString(id2); |
| std::string dummy_key = |
| base::Int64ToString(id1) + "," + base::Int64ToString(dummy_id); |
| EXPECT_TRUE(displays->GetDictionary(dummy_key, &layout_value)); |
| |
| display::DisplayLayout stored_layout; |
| EXPECT_TRUE(display::JsonToDisplayLayout(*layout_value, &stored_layout)); |
| ASSERT_EQ(1u, stored_layout.placement_list.size()); |
| |
| EXPECT_EQ(dummy_layout->placement_list[0].position, |
| stored_layout.placement_list[0].position); |
| EXPECT_EQ(dummy_layout->placement_list[0].offset, |
| stored_layout.placement_list[0].offset); |
| |
| const base::ListValue* external_display_mirror_info = |
| local_state()->GetList(prefs::kExternalDisplayMirrorInfo); |
| EXPECT_EQ(0U, external_display_mirror_info->GetSize()); |
| |
| const base::DictionaryValue* properties = |
| local_state()->GetDictionary(prefs::kDisplayProperties); |
| const base::DictionaryValue* property = nullptr; |
| EXPECT_TRUE(properties->GetDictionary(base::Int64ToString(id1), &property)); |
| int ui_scale = 0; |
| int rotation = 0; |
| EXPECT_TRUE(property->GetInteger("rotation", &rotation)); |
| EXPECT_TRUE(property->GetInteger("ui-scale", &ui_scale)); |
| EXPECT_EQ(1, rotation); |
| |
| EXPECT_EQ(-1000, ui_scale); |
| double display_zoom_1; |
| EXPECT_TRUE(property->GetDouble("display_zoom_factor", &display_zoom_1)); |
| EXPECT_NEAR(display_zoom_1, zoom_factor_1, 0.0001); |
| |
| // Internal display never registered the resolution. |
| int width = 0, height = 0; |
| EXPECT_FALSE(property->GetInteger("width", &width)); |
| EXPECT_FALSE(property->GetInteger("height", &height)); |
| |
| int top = 0, left = 0, bottom = 0, right = 0; |
| EXPECT_TRUE(property->GetInteger("insets_top", &top)); |
| EXPECT_TRUE(property->GetInteger("insets_left", &left)); |
| EXPECT_TRUE(property->GetInteger("insets_bottom", &bottom)); |
| EXPECT_TRUE(property->GetInteger("insets_right", &right)); |
| EXPECT_EQ(10, top); |
| EXPECT_EQ(11, left); |
| EXPECT_EQ(12, bottom); |
| EXPECT_EQ(13, right); |
| |
| display::TouchDeviceManager* tdm = display_manager()->touch_device_manager(); |
| display::test::TouchDeviceManagerTestApi tdm_test_api(tdm); |
| display::TouchDeviceManager::TouchAssociationMap |
| expected_touch_associations_map = tdm->touch_associations(); |
| display::TouchDeviceManager::PortAssociationMap |
| expected_port_associations_map = tdm->port_associations(); |
| tdm_test_api.ResetTouchDeviceManager(); |
| |
| EXPECT_FALSE(CompareTouchAssociations(expected_touch_associations_map, |
| tdm->touch_associations())); |
| EXPECT_FALSE(ComparePortAssociations(expected_port_associations_map, |
| tdm->port_associations())); |
| |
| display_prefs()->LoadTouchAssociationPreferenceForTest(); |
| |
| display::TouchDeviceManager::TouchAssociationMap |
| actual_touch_associations_map = tdm->touch_associations(); |
| |
| EXPECT_TRUE(CompareTouchAssociations(actual_touch_associations_map, |
| expected_touch_associations_map)); |
| EXPECT_TRUE(ComparePortAssociations(expected_port_associations_map, |
| tdm->port_associations())); |
| |
| std::string touch_str; |
| |
| EXPECT_TRUE(properties->GetDictionary(base::Int64ToString(id2), &property)); |
| EXPECT_TRUE(property->GetInteger("rotation", &rotation)); |
| EXPECT_TRUE(property->GetInteger("ui-scale", &ui_scale)); |
| EXPECT_EQ(0, rotation); |
| // ui_scale works only on 2x scale factor/1st display. |
| EXPECT_EQ(-1000, ui_scale); |
| |
| double display_zoom_2; |
| EXPECT_TRUE(property->GetDouble("display_zoom_factor", &display_zoom_2)); |
| EXPECT_NEAR(display_zoom_2, zoom_factor_2, 0.0001); |
| |
| EXPECT_FALSE(property->GetInteger("insets_top", &top)); |
| EXPECT_FALSE(property->GetInteger("insets_left", &left)); |
| EXPECT_FALSE(property->GetInteger("insets_bottom", &bottom)); |
| EXPECT_FALSE(property->GetInteger("insets_right", &right)); |
| |
| // Resolution is saved only when the resolution is set |
| // by DisplayManager::SetDisplayMode |
| width = 0; |
| height = 0; |
| EXPECT_FALSE(property->GetInteger("width", &width)); |
| EXPECT_FALSE(property->GetInteger("height", &height)); |
| |
| display::ManagedDisplayMode mode(gfx::Size(300, 200), 60.0f, false, true, |
| 1.25f /* device_scale_factor */); |
| display_manager()->SetDisplayMode(id2, mode); |
| |
| window_tree_host_manager->SetPrimaryDisplayId(id2); |
| |
| EXPECT_EQ(id2, display::Screen::GetScreen()->GetPrimaryDisplay().id()); |
| |
| EXPECT_TRUE(properties->GetDictionary(base::Int64ToString(id1), &property)); |
| width = 0; |
| height = 0; |
| // Internal display shouldn't store its resolution. |
| EXPECT_FALSE(property->GetInteger("width", &width)); |
| EXPECT_FALSE(property->GetInteger("height", &height)); |
| |
| // External display's resolution must be stored this time because |
| // it's not best. |
| int device_scale_factor = 0; |
| EXPECT_TRUE(properties->GetDictionary(base::Int64ToString(id2), &property)); |
| EXPECT_TRUE(property->GetInteger("width", &width)); |
| EXPECT_TRUE(property->GetInteger("height", &height)); |
| EXPECT_TRUE( |
| property->GetInteger("device-scale-factor", &device_scale_factor)); |
| EXPECT_EQ(300, width); |
| EXPECT_EQ(200, height); |
| EXPECT_EQ(1250, device_scale_factor); |
| |
| // The layout is swapped. |
| EXPECT_TRUE(displays->GetDictionary(key, &layout_value)); |
| |
| EXPECT_TRUE(display::JsonToDisplayLayout(*layout_value, &stored_layout)); |
| ASSERT_EQ(1u, stored_layout.placement_list.size()); |
| const display::DisplayPlacement& stored_placement = |
| stored_layout.placement_list[0]; |
| EXPECT_EQ(display::DisplayPlacement::BOTTOM, stored_placement.position); |
| EXPECT_EQ(-10, stored_placement.offset); |
| EXPECT_EQ(id1, stored_placement.display_id); |
| EXPECT_EQ(id2, stored_placement.parent_display_id); |
| EXPECT_EQ(id2, stored_layout.primary_id); |
| |
| if (true) |
| return; |
| |
| std::string primary_id_str; |
| EXPECT_TRUE(layout_value->GetString(kPrimaryIdKey, &primary_id_str)); |
| EXPECT_EQ(base::Int64ToString(id2), primary_id_str); |
| |
| display_manager()->SetLayoutForCurrentDisplays( |
| display::test::CreateDisplayLayout(ash::Shell::Get()->display_manager(), |
| display::DisplayPlacement::BOTTOM, |
| 20)); |
| |
| UpdateDisplay("1+0-200x200*2,1+0-200x200"); |
| // Mirrored. |
| int offset = 0; |
| std::string position; |
| EXPECT_TRUE(displays->GetDictionary(key, &layout_value)); |
| EXPECT_TRUE(layout_value->GetString(kPositionKey, &position)); |
| EXPECT_EQ("bottom", position); |
| EXPECT_TRUE(layout_value->GetInteger(kOffsetKey, &offset)); |
| EXPECT_EQ(20, offset); |
| std::string id; |
| EXPECT_TRUE(layout_value->GetString(kPlacementDisplayIdKey, &id)); |
| EXPECT_EQ(base::Int64ToString(id1), id); |
| EXPECT_TRUE(layout_value->GetString(kPlacementParentDisplayIdKey, &id)); |
| EXPECT_EQ(base::Int64ToString(id2), id); |
| |
| EXPECT_TRUE(layout_value->GetString(kPrimaryIdKey, &primary_id_str)); |
| EXPECT_EQ(base::Int64ToString(id2), primary_id_str); |
| |
| EXPECT_TRUE(properties->GetDictionary(base::Int64ToString(id1), &property)); |
| EXPECT_FALSE(property->GetInteger("width", &width)); |
| EXPECT_FALSE(property->GetInteger("height", &height)); |
| |
| external_display_mirror_info = |
| local_state()->GetList(prefs::kExternalDisplayMirrorInfo); |
| EXPECT_EQ(1U, external_display_mirror_info->GetSize()); |
| EXPECT_EQ(base::Int64ToString(id2), |
| external_display_mirror_info->GetList()[0].GetString()); |
| |
| // External display's selected resolution must not change |
| // by mirroring. |
| EXPECT_TRUE(properties->GetDictionary(base::Int64ToString(id2), &property)); |
| EXPECT_TRUE(property->GetInteger("width", &width)); |
| EXPECT_TRUE(property->GetInteger("height", &height)); |
| EXPECT_EQ(300, width); |
| EXPECT_EQ(200, height); |
| |
| // Set new display's selected resolution. |
| display_manager()->RegisterDisplayProperty( |
| id2 + 1, display::Display::ROTATE_0, 1.0f, nullptr, gfx::Size(500, 400), |
| 1.0f, 1.0f); |
| |
| UpdateDisplay("200x200*2, 600x500#600x500|500x400"); |
| |
| // Update key as the 2nd display gets new id. |
| id2 = display_manager()->GetSecondaryDisplay().id(); |
| key = base::Int64ToString(id1) + "," + base::Int64ToString(id2); |
| EXPECT_TRUE(displays->GetDictionary(key, &layout_value)); |
| EXPECT_TRUE(layout_value->GetString(kPositionKey, &position)); |
| EXPECT_EQ("right", position); |
| EXPECT_TRUE(layout_value->GetInteger(kOffsetKey, &offset)); |
| EXPECT_EQ(0, offset); |
| EXPECT_TRUE(layout_value->GetString(kPrimaryIdKey, &primary_id_str)); |
| EXPECT_EQ(base::Int64ToString(id1), primary_id_str); |
| |
| // Best resolution should not be saved. |
| EXPECT_TRUE(properties->GetDictionary(base::Int64ToString(id2), &property)); |
| EXPECT_FALSE(property->GetInteger("width", &width)); |
| EXPECT_FALSE(property->GetInteger("height", &height)); |
| |
| // Set yet another new display's selected resolution. |
| display_manager()->RegisterDisplayProperty( |
| id2 + 1, display::Display::ROTATE_0, 1.0f, nullptr, gfx::Size(500, 400), |
| 1.0f, 1.0f); |
| // Disconnect 2nd display first to generate new id for external display. |
| UpdateDisplay("200x200*2"); |
| UpdateDisplay("200x200*2, 500x400#600x500|500x400%60.0f"); |
| // Update key as the 2nd display gets new id. |
| id2 = display_manager()->GetSecondaryDisplay().id(); |
| key = base::Int64ToString(id1) + "," + base::Int64ToString(id2); |
| EXPECT_TRUE(displays->GetDictionary(key, &layout_value)); |
| EXPECT_TRUE(layout_value->GetString(kPositionKey, &position)); |
| EXPECT_EQ("right", position); |
| EXPECT_TRUE(layout_value->GetInteger(kOffsetKey, &offset)); |
| EXPECT_EQ(0, offset); |
| EXPECT_TRUE(layout_value->GetString(kPrimaryIdKey, &primary_id_str)); |
| EXPECT_EQ(base::Int64ToString(id1), primary_id_str); |
| |
| // External display's selected resolution must be updated. |
| EXPECT_TRUE(properties->GetDictionary(base::Int64ToString(id2), &property)); |
| EXPECT_TRUE(property->GetInteger("width", &width)); |
| EXPECT_TRUE(property->GetInteger("height", &height)); |
| EXPECT_EQ(500, width); |
| EXPECT_EQ(400, height); |
| } |
| |
| TEST_F(DisplayPrefsTest, PreventStore) { |
| ResolutionNotificationController::SuppressTimerForTest(); |
| LoggedInAsUser(); |
| UpdateDisplay("400x300#500x400|400x300|300x200"); |
| int64_t id = display::Screen::GetScreen()->GetPrimaryDisplay().id(); |
| // Set display's resolution in single display. It creates the notification and |
| // display preferences should not stored meanwhile. |
| ash::Shell* shell = ash::Shell::Get(); |
| |
| display::ManagedDisplayMode old_mode(gfx::Size(400, 300)); |
| display::ManagedDisplayMode new_mode(gfx::Size(500, 400)); |
| EXPECT_TRUE(shell->resolution_notification_controller() |
| ->PrepareNotificationAndSetDisplayMode(id, old_mode, new_mode, |
| base::OnceClosure())); |
| UpdateDisplay("500x400#500x400|400x300|300x200"); |
| |
| const base::DictionaryValue* properties = |
| local_state()->GetDictionary(prefs::kDisplayProperties); |
| const base::DictionaryValue* property = nullptr; |
| EXPECT_TRUE(properties->GetDictionary(base::Int64ToString(id), &property)); |
| int width = 0, height = 0; |
| EXPECT_FALSE(property->GetInteger("width", &width)); |
| EXPECT_FALSE(property->GetInteger("height", &height)); |
| |
| // Revert the change. |
| shell->resolution_notification_controller()->RevertResolutionChange(false); |
| base::RunLoop().RunUntilIdle(); |
| |
| // The specified resolution will be stored by SetDisplayMode. |
| ash::Shell::Get()->display_manager()->SetDisplayMode( |
| id, display::ManagedDisplayMode(gfx::Size(300, 200), 60.0f, false, true)); |
| UpdateDisplay("300x200#500x400|400x300|300x200"); |
| |
| property = nullptr; |
| EXPECT_TRUE(properties->GetDictionary(base::Int64ToString(id), &property)); |
| EXPECT_TRUE(property->GetInteger("width", &width)); |
| EXPECT_TRUE(property->GetInteger("height", &height)); |
| EXPECT_EQ(300, width); |
| EXPECT_EQ(200, height); |
| } |
| |
| TEST_F(DisplayPrefsTest, StoreForSwappedDisplay) { |
| UpdateDisplay("100x100,200x200"); |
| int64_t id1 = display::Screen::GetScreen()->GetPrimaryDisplay().id(); |
| int64_t id2 = display_manager()->GetSecondaryDisplay().id(); |
| |
| LoggedInAsUser(); |
| |
| SwapPrimaryDisplay(); |
| ASSERT_EQ(id1, display_manager()->GetSecondaryDisplay().id()); |
| |
| std::string key = base::Int64ToString(id1) + "," + base::Int64ToString(id2); |
| const base::DictionaryValue* displays = |
| local_state()->GetDictionary(prefs::kSecondaryDisplays); |
| // Initial saved value is swapped. |
| { |
| const base::DictionaryValue* new_value = nullptr; |
| EXPECT_TRUE(displays->GetDictionary(key, &new_value)); |
| display::DisplayLayout stored_layout; |
| EXPECT_TRUE(display::JsonToDisplayLayout(*new_value, &stored_layout)); |
| ASSERT_EQ(1u, stored_layout.placement_list.size()); |
| const display::DisplayPlacement& stored_placement = |
| stored_layout.placement_list[0]; |
| EXPECT_EQ(display::DisplayPlacement::LEFT, stored_placement.position); |
| EXPECT_EQ(0, stored_placement.offset); |
| EXPECT_EQ(id1, stored_placement.display_id); |
| EXPECT_EQ(id2, stored_placement.parent_display_id); |
| EXPECT_EQ(id2, stored_layout.primary_id); |
| } |
| |
| // Updating layout with primary swapped should save the correct value. |
| { |
| display_manager()->SetLayoutForCurrentDisplays( |
| display::test::CreateDisplayLayout(display_manager(), |
| display::DisplayPlacement::TOP, 10)); |
| const base::DictionaryValue* new_value = nullptr; |
| EXPECT_TRUE(displays->GetDictionary(key, &new_value)); |
| display::DisplayLayout stored_layout; |
| EXPECT_TRUE(display::JsonToDisplayLayout(*new_value, &stored_layout)); |
| ASSERT_EQ(1u, stored_layout.placement_list.size()); |
| const display::DisplayPlacement& stored_placement = |
| stored_layout.placement_list[0]; |
| EXPECT_EQ(display::DisplayPlacement::TOP, stored_placement.position); |
| EXPECT_EQ(10, stored_placement.offset); |
| EXPECT_EQ(id1, stored_placement.display_id); |
| EXPECT_EQ(id2, stored_placement.parent_display_id); |
| EXPECT_EQ(id2, stored_layout.primary_id); |
| } |
| |
| // Swapping primary will save the swapped value. |
| { |
| SwapPrimaryDisplay(); |
| const base::DictionaryValue* new_value = nullptr; |
| EXPECT_TRUE(displays->GetDictionary(key, &new_value)); |
| display::DisplayLayout stored_layout; |
| |
| EXPECT_TRUE(displays->GetDictionary(key, &new_value)); |
| EXPECT_TRUE(display::JsonToDisplayLayout(*new_value, &stored_layout)); |
| ASSERT_EQ(1u, stored_layout.placement_list.size()); |
| const display::DisplayPlacement& stored_placement = |
| stored_layout.placement_list[0]; |
| EXPECT_EQ(display::DisplayPlacement::BOTTOM, stored_placement.position); |
| EXPECT_EQ(-10, stored_placement.offset); |
| EXPECT_EQ(id2, stored_placement.display_id); |
| EXPECT_EQ(id1, stored_placement.parent_display_id); |
| EXPECT_EQ(id1, stored_layout.primary_id); |
| } |
| } |
| |
| TEST_F(DisplayPrefsTestGuest, DisplayPrefsTestGuest) { |
| ash::WindowTreeHostManager* window_tree_host_manager = |
| ash::Shell::Get()->window_tree_host_manager(); |
| |
| UpdateDisplay("200x200*2,200x200"); |
| |
| LoggedInAsGuest(); |
| int64_t id1 = display::Screen::GetScreen()->GetPrimaryDisplay().id(); |
| display::test::ScopedSetInternalDisplayId set_internal( |
| ash::Shell::Get()->display_manager(), id1); |
| int64_t id2 = display_manager()->GetSecondaryDisplay().id(); |
| display_manager()->SetLayoutForCurrentDisplays( |
| display::test::CreateDisplayLayout(display_manager(), |
| display::DisplayPlacement::TOP, 10)); |
| const float scale = 1.25f; |
| display_manager()->UpdateZoomFactor(id1, 1.f / scale); |
| window_tree_host_manager->SetPrimaryDisplayId(id2); |
| int64_t new_primary = display::Screen::GetScreen()->GetPrimaryDisplay().id(); |
| window_tree_host_manager->SetOverscanInsets(new_primary, |
| gfx::Insets(10, 11, 12, 13)); |
| display_manager()->SetDisplayRotation(new_primary, |
| display::Display::ROTATE_90, |
| display::Display::RotationSource::USER); |
| |
| // Does not store the preferences locally. |
| EXPECT_FALSE(local_state() |
| ->FindPreference(prefs::kSecondaryDisplays) |
| ->HasUserSetting()); |
| EXPECT_FALSE(local_state() |
| ->FindPreference(prefs::kDisplayProperties) |
| ->HasUserSetting()); |
| |
| // Settings are still notified to the system. |
| display::Screen* screen = display::Screen::GetScreen(); |
| EXPECT_EQ(id2, screen->GetPrimaryDisplay().id()); |
| const display::DisplayPlacement& placement = |
| display_manager()->GetCurrentDisplayLayout().placement_list[0]; |
| EXPECT_EQ(display::DisplayPlacement::BOTTOM, placement.position); |
| EXPECT_EQ(-10, placement.offset); |
| const display::Display& primary_display = screen->GetPrimaryDisplay(); |
| EXPECT_EQ("178x176", primary_display.bounds().size().ToString()); |
| EXPECT_EQ(display::Display::ROTATE_90, primary_display.rotation()); |
| |
| const display::ManagedDisplayInfo& info1 = |
| display_manager()->GetDisplayInfo(id1); |
| EXPECT_FLOAT_EQ(1.f / scale, info1.zoom_factor()); |
| |
| const display::ManagedDisplayInfo& info_primary = |
| display_manager()->GetDisplayInfo(new_primary); |
| EXPECT_EQ(display::Display::ROTATE_90, info_primary.GetActiveRotation()); |
| EXPECT_EQ(1.0f, info_primary.zoom_factor()); |
| } |
| |
| TEST_F(DisplayPrefsTest, StorePowerStateNoLogin) { |
| EXPECT_FALSE(local_state()->HasPrefPath(prefs::kDisplayPowerState)); |
| |
| // Stores display prefs without login, which still stores the power state. |
| display_prefs()->MaybeStoreDisplayPrefs(); |
| EXPECT_TRUE(local_state()->HasPrefPath(prefs::kDisplayPowerState)); |
| } |
| |
| TEST_F(DisplayPrefsTest, StorePowerStateGuest) { |
| EXPECT_FALSE(local_state()->HasPrefPath(prefs::kDisplayPowerState)); |
| |
| LoggedInAsGuest(); |
| display_prefs()->MaybeStoreDisplayPrefs(); |
| EXPECT_TRUE(local_state()->HasPrefPath(prefs::kDisplayPowerState)); |
| } |
| |
| TEST_F(DisplayPrefsTest, StorePowerStateNormalUser) { |
| EXPECT_FALSE(local_state()->HasPrefPath(prefs::kDisplayPowerState)); |
| |
| LoggedInAsUser(); |
| display_prefs()->MaybeStoreDisplayPrefs(); |
| EXPECT_TRUE(local_state()->HasPrefPath(prefs::kDisplayPowerState)); |
| } |
| |
| TEST_F(DisplayPrefsTest, DisplayPowerStateAfterRestart) { |
| display_prefs()->StoreDisplayPowerStateForTest( |
| chromeos::DISPLAY_POWER_INTERNAL_OFF_EXTERNAL_ON); |
| LoadDisplayPreferences(); |
| EXPECT_EQ(chromeos::DISPLAY_POWER_INTERNAL_OFF_EXTERNAL_ON, |
| GetRequestedPowerState()); |
| } |
| |
| TEST_F(DisplayPrefsTest, DontSaveAndRestoreAllOff) { |
| display_prefs()->StoreDisplayPowerStateForTest( |
| chromeos::DISPLAY_POWER_INTERNAL_OFF_EXTERNAL_ON); |
| LoadDisplayPreferences(); |
| // DisplayPowerState should be ignored at boot. |
| EXPECT_EQ(chromeos::DISPLAY_POWER_INTERNAL_OFF_EXTERNAL_ON, |
| GetRequestedPowerState()); |
| |
| display_prefs()->StoreDisplayPowerStateForTest( |
| chromeos::DISPLAY_POWER_ALL_OFF); |
| EXPECT_EQ(chromeos::DISPLAY_POWER_INTERNAL_OFF_EXTERNAL_ON, |
| GetRequestedPowerState()); |
| EXPECT_EQ("internal_off_external_on", |
| local_state()->GetString(prefs::kDisplayPowerState)); |
| |
| // Don't try to load |
| local_state()->SetString(prefs::kDisplayPowerState, "all_off"); |
| LoadDisplayPreferences(); |
| EXPECT_EQ(chromeos::DISPLAY_POWER_INTERNAL_OFF_EXTERNAL_ON, |
| GetRequestedPowerState()); |
| } |
| |
| // Tests that display configuration changes caused by TabletModeController |
| // are not saved. |
| TEST_F(DisplayPrefsTest, DontSaveTabletModeControllerRotations) { |
| ash::Shell* shell = ash::Shell::Get(); |
| display::Display::SetInternalDisplayId( |
| display::Screen::GetScreen()->GetPrimaryDisplay().id()); |
| LoggedInAsUser(); |
| // Populate the properties. |
| display_manager()->SetDisplayRotation(display::Display::InternalDisplayId(), |
| display::Display::ROTATE_180, |
| display::Display::RotationSource::USER); |
| // Reset property to avoid rotation lock |
| display_manager()->SetDisplayRotation(display::Display::InternalDisplayId(), |
| display::Display::ROTATE_0, |
| display::Display::RotationSource::USER); |
| |
| // Open up 270 degrees to trigger tablet mode |
| scoped_refptr<chromeos::AccelerometerUpdate> update( |
| new chromeos::AccelerometerUpdate()); |
| update->Set(chromeos::ACCELEROMETER_SOURCE_ATTACHED_KEYBOARD, 0.0f, 0.0f, |
| kMeanGravity); |
| update->Set(chromeos::ACCELEROMETER_SOURCE_SCREEN, 0.0f, -kMeanGravity, 0.0f); |
| ash::TabletModeController* controller = |
| ash::Shell::Get()->tablet_mode_controller(); |
| controller->OnAccelerometerUpdated(update); |
| EXPECT_TRUE(controller->IsTabletModeWindowManagerEnabled()); |
| |
| // Trigger 90 degree rotation |
| update->Set(chromeos::ACCELEROMETER_SOURCE_ATTACHED_KEYBOARD, -kMeanGravity, |
| 0.0f, 0.0f); |
| update->Set(chromeos::ACCELEROMETER_SOURCE_SCREEN, -kMeanGravity, 0.0f, 0.0f); |
| controller->OnAccelerometerUpdated(update); |
| shell->screen_orientation_controller()->OnAccelerometerUpdated(update); |
| EXPECT_EQ(display::Display::ROTATE_90, GetCurrentInternalDisplayRotation()); |
| |
| const base::DictionaryValue* properties = |
| local_state()->GetDictionary(prefs::kDisplayProperties); |
| const base::DictionaryValue* property = nullptr; |
| EXPECT_TRUE(properties->GetDictionary( |
| base::Int64ToString(display::Display::InternalDisplayId()), &property)); |
| int rotation = -1; |
| EXPECT_TRUE(property->GetInteger("rotation", &rotation)); |
| EXPECT_EQ(display::Display::ROTATE_0, rotation); |
| |
| // Trigger a save, the acceleration rotation should not be saved as the user |
| // rotation. |
| display_prefs()->MaybeStoreDisplayPrefs(); |
| properties = local_state()->GetDictionary(prefs::kDisplayProperties); |
| property = nullptr; |
| EXPECT_TRUE(properties->GetDictionary( |
| base::Int64ToString(display::Display::InternalDisplayId()), &property)); |
| rotation = -1; |
| EXPECT_TRUE(property->GetInteger("rotation", &rotation)); |
| EXPECT_EQ(display::Display::ROTATE_0, rotation); |
| } |
| |
| // Tests that the rotation state is saved without a user being logged in. |
| TEST_F(DisplayPrefsTest, StoreRotationStateNoLogin) { |
| display::Display::SetInternalDisplayId( |
| display::Screen::GetScreen()->GetPrimaryDisplay().id()); |
| EXPECT_FALSE(local_state()->HasPrefPath(prefs::kDisplayRotationLock)); |
| |
| bool current_rotation_lock = IsRotationLocked(); |
| display_prefs()->StoreDisplayRotationPrefsForTest(GetRotation(), |
| current_rotation_lock); |
| EXPECT_TRUE(local_state()->HasPrefPath(prefs::kDisplayRotationLock)); |
| |
| const base::DictionaryValue* properties = |
| local_state()->GetDictionary(prefs::kDisplayRotationLock); |
| bool rotation_lock; |
| EXPECT_TRUE(properties->GetBoolean("lock", &rotation_lock)); |
| EXPECT_EQ(current_rotation_lock, rotation_lock); |
| |
| int orientation; |
| display::Display::Rotation current_rotation = |
| GetCurrentInternalDisplayRotation(); |
| EXPECT_TRUE(properties->GetInteger("orientation", &orientation)); |
| EXPECT_EQ(current_rotation, orientation); |
| } |
| |
| // Tests that the rotation state is saved when a guest is logged in. |
| TEST_F(DisplayPrefsTest, StoreRotationStateGuest) { |
| display::Display::SetInternalDisplayId( |
| display::Screen::GetScreen()->GetPrimaryDisplay().id()); |
| EXPECT_FALSE(local_state()->HasPrefPath(prefs::kDisplayRotationLock)); |
| LoggedInAsGuest(); |
| |
| bool current_rotation_lock = IsRotationLocked(); |
| display_prefs()->StoreDisplayRotationPrefsForTest(GetRotation(), |
| current_rotation_lock); |
| EXPECT_TRUE(local_state()->HasPrefPath(prefs::kDisplayRotationLock)); |
| |
| const base::DictionaryValue* properties = |
| local_state()->GetDictionary(prefs::kDisplayRotationLock); |
| bool rotation_lock; |
| EXPECT_TRUE(properties->GetBoolean("lock", &rotation_lock)); |
| EXPECT_EQ(current_rotation_lock, rotation_lock); |
| |
| int orientation; |
| display::Display::Rotation current_rotation = |
| GetCurrentInternalDisplayRotation(); |
| EXPECT_TRUE(properties->GetInteger("orientation", &orientation)); |
| EXPECT_EQ(current_rotation, orientation); |
| } |
| |
| // Tests that the rotation state is saved when a normal user is logged in. |
| TEST_F(DisplayPrefsTest, StoreRotationStateNormalUser) { |
| display::Display::SetInternalDisplayId( |
| display::Screen::GetScreen()->GetPrimaryDisplay().id()); |
| EXPECT_FALSE(local_state()->HasPrefPath(prefs::kDisplayRotationLock)); |
| LoggedInAsGuest(); |
| |
| bool current_rotation_lock = IsRotationLocked(); |
| display_prefs()->StoreDisplayRotationPrefsForTest(GetRotation(), |
| current_rotation_lock); |
| EXPECT_TRUE(local_state()->HasPrefPath(prefs::kDisplayRotationLock)); |
| |
| const base::DictionaryValue* properties = |
| local_state()->GetDictionary(prefs::kDisplayRotationLock); |
| bool rotation_lock; |
| EXPECT_TRUE(properties->GetBoolean("lock", &rotation_lock)); |
| EXPECT_EQ(current_rotation_lock, rotation_lock); |
| |
| int orientation; |
| display::Display::Rotation current_rotation = |
| GetCurrentInternalDisplayRotation(); |
| EXPECT_TRUE(properties->GetInteger("orientation", &orientation)); |
| EXPECT_EQ(current_rotation, orientation); |
| } |
| |
| // Tests that rotation state is loaded without a user being logged in, and that |
| // entering tablet mode applies the state. |
| TEST_F(DisplayPrefsTest, LoadRotationNoLogin) { |
| display::Display::SetInternalDisplayId( |
| display::Screen::GetScreen()->GetPrimaryDisplay().id()); |
| ASSERT_FALSE(local_state()->HasPrefPath(prefs::kDisplayRotationLock)); |
| |
| bool initial_rotation_lock = IsRotationLocked(); |
| ASSERT_FALSE(initial_rotation_lock); |
| display::Display::Rotation initial_rotation = |
| GetCurrentInternalDisplayRotation(); |
| ASSERT_EQ(display::Display::ROTATE_0, initial_rotation); |
| |
| display_prefs()->StoreDisplayRotationPrefsForTest(GetRotation(), |
| initial_rotation_lock); |
| ASSERT_TRUE(local_state()->HasPrefPath(prefs::kDisplayRotationLock)); |
| |
| display_prefs()->StoreDisplayRotationPrefsForTest(display::Display::ROTATE_90, |
| true); |
| LoadDisplayPreferences(); |
| |
| bool display_rotation_lock = |
| display_manager()->registered_internal_display_rotation_lock(); |
| bool display_rotation = |
| display_manager()->registered_internal_display_rotation(); |
| EXPECT_TRUE(display_rotation_lock); |
| EXPECT_EQ(display::Display::ROTATE_90, display_rotation); |
| |
| bool rotation_lock = IsRotationLocked(); |
| display::Display::Rotation before_tablet_mode_rotation = |
| GetCurrentInternalDisplayRotation(); |
| |
| // Settings should not be applied until tablet mode activates |
| EXPECT_FALSE(rotation_lock); |
| EXPECT_EQ(display::Display::ROTATE_0, before_tablet_mode_rotation); |
| |
| // Open up 270 degrees to trigger tablet mode |
| scoped_refptr<chromeos::AccelerometerUpdate> update( |
| new chromeos::AccelerometerUpdate()); |
| update->Set(chromeos::ACCELEROMETER_SOURCE_ATTACHED_KEYBOARD, 0.0f, 0.0f, |
| kMeanGravity); |
| update->Set(chromeos::ACCELEROMETER_SOURCE_SCREEN, 0.0f, -kMeanGravity, 0.0f); |
| ash::TabletModeController* tablet_mode_controller = |
| ash::Shell::Get()->tablet_mode_controller(); |
| tablet_mode_controller->OnAccelerometerUpdated(update); |
| EXPECT_TRUE(tablet_mode_controller->IsTabletModeWindowManagerEnabled()); |
| bool screen_orientation_rotation_lock = IsRotationLocked(); |
| display::Display::Rotation tablet_mode_rotation = |
| GetCurrentInternalDisplayRotation(); |
| EXPECT_TRUE(screen_orientation_rotation_lock); |
| EXPECT_EQ(display::Display::ROTATE_90, tablet_mode_rotation); |
| } |
| |
| // Tests that rotation lock being set causes the rotation state to be saved. |
| TEST_F(DisplayPrefsTest, RotationLockTriggersStore) { |
| display::Display::SetInternalDisplayId( |
| display::Screen::GetScreen()->GetPrimaryDisplay().id()); |
| ASSERT_FALSE(local_state()->HasPrefPath(prefs::kDisplayRotationLock)); |
| |
| ash::Shell::Get()->screen_orientation_controller()->ToggleUserRotationLock(); |
| |
| EXPECT_TRUE(local_state()->HasPrefPath(prefs::kDisplayRotationLock)); |
| |
| const base::DictionaryValue* properties = |
| local_state()->GetDictionary(prefs::kDisplayRotationLock); |
| bool rotation_lock; |
| EXPECT_TRUE(properties->GetBoolean("lock", &rotation_lock)); |
| } |
| |
| TEST_F(DisplayPrefsTest, SaveUnifiedMode) { |
| LoggedInAsUser(); |
| display_manager()->SetUnifiedDesktopEnabled(true); |
| |
| UpdateDisplay("200x200,100x100"); |
| display::DisplayIdList list = display_manager()->GetCurrentDisplayIdList(); |
| EXPECT_EQ( |
| "400x200", |
| display::Screen::GetScreen()->GetPrimaryDisplay().size().ToString()); |
| |
| const base::DictionaryValue* secondary_displays = |
| local_state()->GetDictionary(prefs::kSecondaryDisplays); |
| const base::DictionaryValue* new_value = nullptr; |
| EXPECT_TRUE(secondary_displays->GetDictionary( |
| display::DisplayIdListToString(list), &new_value)); |
| |
| display::DisplayLayout stored_layout; |
| EXPECT_TRUE(display::JsonToDisplayLayout(*new_value, &stored_layout)); |
| EXPECT_TRUE(stored_layout.default_unified); |
| |
| const base::DictionaryValue* displays = |
| local_state()->GetDictionary(prefs::kDisplayProperties); |
| int64_t unified_id = display::Screen::GetScreen()->GetPrimaryDisplay().id(); |
| EXPECT_FALSE( |
| displays->GetDictionary(base::Int64ToString(unified_id), &new_value)); |
| |
| display::test::SetDisplayResolution(display_manager(), unified_id, |
| gfx::Size(200, 100)); |
| EXPECT_EQ( |
| "200x100", |
| display::Screen::GetScreen()->GetPrimaryDisplay().size().ToString()); |
| EXPECT_FALSE( |
| displays->GetDictionary(base::Int64ToString(unified_id), &new_value)); |
| |
| // Mirror mode should remember if the default mode was unified. |
| display_manager()->SetMirrorMode(display::MirrorMode::kNormal, base::nullopt); |
| ASSERT_TRUE(secondary_displays->GetDictionary( |
| display::DisplayIdListToString(list), &new_value)); |
| EXPECT_TRUE(display::JsonToDisplayLayout(*new_value, &stored_layout)); |
| EXPECT_TRUE(stored_layout.default_unified); |
| |
| display_manager()->SetMirrorMode(display::MirrorMode::kOff, base::nullopt); |
| ASSERT_TRUE(secondary_displays->GetDictionary( |
| display::DisplayIdListToString(list), &new_value)); |
| EXPECT_TRUE(display::JsonToDisplayLayout(*new_value, &stored_layout)); |
| EXPECT_TRUE(stored_layout.default_unified); |
| |
| // Exit unified mode. |
| display_manager()->SetDefaultMultiDisplayModeForCurrentDisplays( |
| display::DisplayManager::EXTENDED); |
| ASSERT_TRUE(secondary_displays->GetDictionary( |
| display::DisplayIdListToString(list), &new_value)); |
| EXPECT_TRUE(display::JsonToDisplayLayout(*new_value, &stored_layout)); |
| EXPECT_FALSE(stored_layout.default_unified); |
| } |
| |
| TEST_F(DisplayPrefsTest, RestoreUnifiedMode) { |
| const int64_t first_display_id = 210000001; |
| const int64_t second_display_id = 220000002; |
| display::ManagedDisplayInfo first_display_info = |
| display::CreateDisplayInfo(first_display_id, gfx::Rect(1, 1, 500, 500)); |
| display::ManagedDisplayInfo second_display_info = |
| display::CreateDisplayInfo(second_display_id, gfx::Rect(2, 2, 500, 500)); |
| std::vector<display::ManagedDisplayInfo> display_info_list; |
| display_info_list.emplace_back(first_display_info); |
| display_manager()->OnNativeDisplaysChanged(display_info_list); |
| ash::Shell::Get()->window_tree_host_manager()->SetPrimaryDisplayId( |
| first_display_id); |
| EXPECT_FALSE(display_manager()->IsInUnifiedMode()); |
| EXPECT_FALSE(display_manager()->IsInMirrorMode()); |
| |
| display::DisplayIdList list = |
| display::test::CreateDisplayIdList2(first_display_id, second_display_id); |
| StoreDisplayBoolPropertyForList(list, "default_unified", true); |
| StoreDisplayPropertyForList( |
| list, "primary-id", |
| std::make_unique<base::Value>(base::Int64ToString(first_display_id))); |
| LoadDisplayPreferences(); |
| |
| // Should not restore to unified unless unified desktop is enabled. |
| display_info_list.emplace_back(second_display_info); |
| display_manager()->OnNativeDisplaysChanged(display_info_list); |
| EXPECT_FALSE(display_manager()->IsInUnifiedMode()); |
| |
| // Restored to unified. |
| display_manager()->SetUnifiedDesktopEnabled(true); |
| StoreDisplayBoolPropertyForList(list, "default_unified", true); |
| LoadDisplayPreferences(); |
| display_manager()->OnNativeDisplaysChanged(display_info_list); |
| EXPECT_TRUE(display_manager()->IsInUnifiedMode()); |
| |
| // Remove the second display. |
| display_info_list.erase(display_info_list.end() - 1); |
| display_manager()->OnNativeDisplaysChanged(display_info_list); |
| EXPECT_FALSE(display_manager()->IsInUnifiedMode()); |
| |
| // Restored to mirror, then unified. |
| std::set<int64_t> external_display_mirror_info; |
| external_display_mirror_info.emplace( |
| display::GetDisplayIdWithoutOutputIndex(second_display_id)); |
| StoreExternalDisplayMirrorInfo(external_display_mirror_info); |
| StoreDisplayBoolPropertyForList(list, "default_unified", true); |
| LoadDisplayPreferences(); |
| display_info_list.emplace_back(second_display_info); |
| display_manager()->OnNativeDisplaysChanged(display_info_list); |
| EXPECT_TRUE(display_manager()->IsInMirrorMode()); |
| |
| display_manager()->SetMirrorMode(display::MirrorMode::kOff, base::nullopt); |
| EXPECT_TRUE(display_manager()->IsInUnifiedMode()); |
| |
| // Remove the second display. |
| display_info_list.erase(display_info_list.end() - 1); |
| display_manager()->OnNativeDisplaysChanged(display_info_list); |
| EXPECT_FALSE(display_manager()->IsInUnifiedMode()); |
| |
| // Sanity check. Restore to extended. |
| external_display_mirror_info.clear(); |
| StoreExternalDisplayMirrorInfo(external_display_mirror_info); |
| StoreDisplayBoolPropertyForList(list, "default_unified", false); |
| LoadDisplayPreferences(); |
| display_info_list.emplace_back(second_display_info); |
| display_manager()->OnNativeDisplaysChanged(display_info_list); |
| EXPECT_FALSE(display_manager()->IsInMirrorMode()); |
| EXPECT_FALSE(display_manager()->IsInUnifiedMode()); |
| } |
| |
| TEST_F(DisplayPrefsTest, SaveThreeDisplays) { |
| LoggedInAsUser(); |
| UpdateDisplay("200x200,200x200,300x300"); |
| |
| display::DisplayIdList list = display_manager()->GetCurrentDisplayIdList(); |
| ASSERT_EQ(3u, list.size()); |
| |
| display::DisplayLayoutBuilder builder(list[0]); |
| builder.AddDisplayPlacement(list[1], list[0], |
| display::DisplayPlacement::RIGHT, 0); |
| builder.AddDisplayPlacement(list[2], list[0], |
| display::DisplayPlacement::BOTTOM, 100); |
| display_manager()->SetLayoutForCurrentDisplays(builder.Build()); |
| |
| const base::DictionaryValue* secondary_displays = |
| local_state()->GetDictionary(prefs::kSecondaryDisplays); |
| const base::DictionaryValue* new_value = nullptr; |
| EXPECT_TRUE(secondary_displays->GetDictionary( |
| display::DisplayIdListToString(list), &new_value)); |
| } |
| |
| TEST_F(DisplayPrefsTest, RestoreThreeDisplays) { |
| LoggedInAsUser(); |
| int64_t id1 = display::Screen::GetScreen()->GetPrimaryDisplay().id(); |
| display::DisplayIdList list = |
| display::test::CreateDisplayIdListN(3, id1, id1 + 1, id1 + 2); |
| |
| display::DisplayLayoutBuilder builder(list[0]); |
| builder.AddDisplayPlacement(list[1], list[0], display::DisplayPlacement::LEFT, |
| 0); |
| builder.AddDisplayPlacement(list[2], list[1], |
| display::DisplayPlacement::BOTTOM, 100); |
| display_prefs()->StoreDisplayLayoutPrefForTest(list, *builder.Build()); |
| LoadDisplayPreferences(); |
| |
| UpdateDisplay("200x200,200x200,300x300"); |
| display::DisplayIdList new_list = |
| display_manager()->GetCurrentDisplayIdList(); |
| ASSERT_EQ(3u, list.size()); |
| ASSERT_EQ(list[0], new_list[0]); |
| ASSERT_EQ(list[1], new_list[1]); |
| ASSERT_EQ(list[2], new_list[2]); |
| |
| EXPECT_EQ(gfx::Rect(0, 0, 200, 200), |
| display_manager()->GetDisplayForId(list[0]).bounds()); |
| EXPECT_EQ(gfx::Rect(-200, 0, 200, 200), |
| display_manager()->GetDisplayForId(list[1]).bounds()); |
| EXPECT_EQ(gfx::Rect(-100, 200, 300, 300), |
| display_manager()->GetDisplayForId(list[2]).bounds()); |
| } |
| |
| TEST_F(DisplayPrefsTest, LegacyTouchCalibrationDataSupport) { |
| UpdateDisplay("800x600,1200x800"); |
| LoggedInAsUser(); |
| int64_t id = display::Screen::GetScreen()->GetPrimaryDisplay().id(); |
| display::TouchCalibrationData::CalibrationPointPairQuad point_pair_quad = { |
| {std::make_pair(gfx::Point(10, 10), gfx::Point(11, 12)), |
| std::make_pair(gfx::Point(190, 10), gfx::Point(195, 8)), |
| std::make_pair(gfx::Point(10, 90), gfx::Point(12, 94)), |
| std::make_pair(gfx::Point(190, 90), gfx::Point(189, 88))}}; |
| gfx::Size touch_size(200, 150); |
| display::TouchCalibrationData data(point_pair_quad, touch_size); |
| |
| display_prefs()->StoreLegacyTouchDataForTest(id, data); |
| |
| display::TouchDeviceManager* tdm = display_manager()->touch_device_manager(); |
| display::test::TouchDeviceManagerTestApi tdm_test_api(tdm); |
| tdm_test_api.ResetTouchDeviceManager(); |
| |
| display_prefs()->LoadTouchAssociationPreferenceForTest(); |
| |
| const display::TouchDeviceManager::TouchAssociationMap& association_map = |
| tdm->touch_associations(); |
| |
| const display::TouchDeviceIdentifier& fallback_identifier = |
| display::TouchDeviceIdentifier::GetFallbackTouchDeviceIdentifier(); |
| |
| EXPECT_TRUE(association_map.count(fallback_identifier)); |
| EXPECT_TRUE(association_map.at(fallback_identifier).count(id)); |
| EXPECT_EQ(association_map.at(fallback_identifier).at(id).calibration_data, |
| data); |
| |
| int64_t id_2 = display_manager()->GetSecondaryDisplay().id(); |
| gfx::Size touch_size_2(300, 300); |
| display::TouchCalibrationData data_2(point_pair_quad, touch_size_2); |
| |
| display::TouchDeviceIdentifier identifier(12345); |
| display_manager()->SetTouchCalibrationData(id_2, point_pair_quad, |
| touch_size_2, identifier); |
| |
| EXPECT_TRUE(tdm->touch_associations().count(identifier)); |
| EXPECT_TRUE(tdm->touch_associations().at(identifier).count(id_2)); |
| EXPECT_EQ(tdm->touch_associations().at(identifier).at(id_2).calibration_data, |
| data_2); |
| |
| tdm_test_api.ResetTouchDeviceManager(); |
| EXPECT_TRUE(tdm->touch_associations().empty()); |
| |
| display_prefs()->LoadTouchAssociationPreferenceForTest(); |
| |
| EXPECT_TRUE(tdm->touch_associations().count(fallback_identifier)); |
| EXPECT_TRUE(tdm->touch_associations().at(fallback_identifier).count(id)); |
| EXPECT_EQ( |
| tdm->touch_associations().at(fallback_identifier).at(id).calibration_data, |
| data); |
| EXPECT_TRUE(tdm->touch_associations().count(identifier)); |
| EXPECT_TRUE(tdm->touch_associations().at(identifier).count(id_2)); |
| EXPECT_EQ(tdm->touch_associations().at(identifier).at(id_2).calibration_data, |
| data_2); |
| } |
| |
| TEST_F(DisplayPrefsTest, ExternalDisplayMirrorInfo) { |
| LoggedInAsUser(); |
| |
| base::CommandLine::ForCurrentProcess()->AppendSwitch( |
| chromeos::switches::kFirstExecAfterBoot); |
| |
| const int64_t internal_display_id = |
| display::test::DisplayManagerTestApi(display_manager()) |
| .SetFirstDisplayAsInternalDisplay(); |
| constexpr int64_t first_display_id = 210000001; |
| constexpr int64_t second_display_id = 220000002; |
| const int64_t first_display_masked_id = |
| display::GetDisplayIdWithoutOutputIndex(first_display_id); |
| const int64_t second_display_masked_id = |
| display::GetDisplayIdWithoutOutputIndex(second_display_id); |
| display::ManagedDisplayInfo first_display_info = |
| display::CreateDisplayInfo(first_display_id, gfx::Rect(1, 1, 500, 500)); |
| display::ManagedDisplayInfo second_display_info = |
| display::CreateDisplayInfo(second_display_id, gfx::Rect(2, 2, 500, 500)); |
| std::vector<display::ManagedDisplayInfo> display_info_list; |
| |
| // There's no external display now. |
| display_info_list.push_back(display::CreateDisplayInfo( |
| internal_display_id, gfx::Rect(0, 0, 100, 100))); |
| display_manager()->OnNativeDisplaysChanged(display_info_list); |
| |
| // Add first display id to the external display mirror info. |
| std::set<int64_t> external_display_mirror_info; |
| external_display_mirror_info.emplace(first_display_masked_id); |
| StoreExternalDisplayMirrorInfo(external_display_mirror_info); |
| LoadDisplayPreferences(); |
| const base::ListValue* pref_external_display_mirror_info = |
| local_state()->GetList(prefs::kExternalDisplayMirrorInfo); |
| EXPECT_EQ(1U, pref_external_display_mirror_info->GetSize()); |
| EXPECT_EQ(base::Int64ToString(first_display_masked_id), |
| pref_external_display_mirror_info->GetList()[0].GetString()); |
| |
| // Add first display, mirror mode restores and the external display mirror |
| // info does not change. |
| display_info_list.emplace_back(first_display_info); |
| display_manager()->OnNativeDisplaysChanged(display_info_list); |
| EXPECT_TRUE(display_manager()->IsInMirrorMode()); |
| pref_external_display_mirror_info = |
| local_state()->GetList(prefs::kExternalDisplayMirrorInfo); |
| EXPECT_EQ(1U, pref_external_display_mirror_info->GetSize()); |
| EXPECT_EQ(base::Int64ToString(first_display_masked_id), |
| pref_external_display_mirror_info->GetList()[0].GetString()); |
| |
| // Add second display, mirror mode persists and the second display id is added |
| // to the external display mirror info. |
| display_info_list.emplace_back(second_display_info); |
| display_manager()->OnNativeDisplaysChanged(display_info_list); |
| EXPECT_TRUE(display_manager()->IsInMirrorMode()); |
| pref_external_display_mirror_info = |
| local_state()->GetList(prefs::kExternalDisplayMirrorInfo); |
| EXPECT_EQ(2U, pref_external_display_mirror_info->GetSize()); |
| EXPECT_EQ(base::Int64ToString(first_display_masked_id), |
| pref_external_display_mirror_info->GetList()[0].GetString()); |
| EXPECT_EQ(base::Int64ToString(second_display_masked_id), |
| pref_external_display_mirror_info->GetList()[1].GetString()); |
| |
| // Disconnect all external displays. |
| display_info_list.erase(display_info_list.begin() + 1, |
| display_info_list.end()); |
| display_manager()->OnNativeDisplaysChanged(display_info_list); |
| |
| // Clear external display mirror info and only add second display id to it. |
| external_display_mirror_info.clear(); |
| external_display_mirror_info.emplace(second_display_masked_id); |
| StoreExternalDisplayMirrorInfo(external_display_mirror_info); |
| LoadDisplayPreferences(); |
| pref_external_display_mirror_info = |
| local_state()->GetList(prefs::kExternalDisplayMirrorInfo); |
| EXPECT_EQ(1U, pref_external_display_mirror_info->GetSize()); |
| EXPECT_EQ(base::Int64ToString(second_display_masked_id), |
| pref_external_display_mirror_info->GetList()[0].GetString()); |
| |
| // Add first display, mirror mode is off and the external display mirror info |
| // does not change. |
| display_info_list.emplace_back(first_display_info); |
| display_manager()->OnNativeDisplaysChanged(display_info_list); |
| EXPECT_FALSE(display_manager()->IsInMirrorMode()); |
| pref_external_display_mirror_info = |
| local_state()->GetList(prefs::kExternalDisplayMirrorInfo); |
| EXPECT_EQ(1U, pref_external_display_mirror_info->GetSize()); |
| EXPECT_EQ(base::Int64ToString(second_display_masked_id), |
| pref_external_display_mirror_info->GetList()[0].GetString()); |
| |
| // Add second display, mirror mode remains off and the second display id is |
| // removed from the external display mirror info. |
| display_info_list.emplace_back(second_display_info); |
| display_manager()->OnNativeDisplaysChanged(display_info_list); |
| EXPECT_FALSE(display_manager()->IsInMirrorMode()); |
| pref_external_display_mirror_info = |
| local_state()->GetList(prefs::kExternalDisplayMirrorInfo); |
| EXPECT_EQ(0U, pref_external_display_mirror_info->GetSize()); |
| } |
| |
| TEST_F(DisplayPrefsTest, DisplayMixedMirrorMode) { |
| LoggedInAsUser(); |
| |
| const int64_t internal_display_id = |
| display::test::DisplayManagerTestApi(display_manager()) |
| .SetFirstDisplayAsInternalDisplay(); |
| constexpr int64_t first_display_id = 210000001; |
| constexpr int64_t second_display_id = 220000002; |
| std::vector<display::ManagedDisplayInfo> display_info_list; |
| display_info_list.push_back(display::CreateDisplayInfo( |
| internal_display_id, gfx::Rect(0, 0, 100, 100))); |
| display_info_list.push_back( |
| display::CreateDisplayInfo(first_display_id, gfx::Rect(1, 1, 500, 500))); |
| display_info_list.push_back( |
| display::CreateDisplayInfo(second_display_id, gfx::Rect(2, 2, 500, 500))); |
| |
| // Store mixed mirror mode parameters which specify mirroring from the |
| // internal display to the first external display. |
| display::DisplayIdList dst_ids; |
| dst_ids.emplace_back(first_display_id); |
| base::Optional<display::MixedMirrorModeParams> mixed_params( |
| base::in_place, internal_display_id, dst_ids); |
| display_prefs()->StoreDisplayMixedMirrorModeParamsForTest(mixed_params); |
| LoadDisplayPreferences(); |
| |
| // Connect both first and second external display. Mixed mirror mode is |
| // restored. |
| display_manager()->OnNativeDisplaysChanged(display_info_list); |
| EXPECT_TRUE(display_manager()->IsInSoftwareMirrorMode()); |
| EXPECT_EQ(internal_display_id, display_manager()->mirroring_source_id()); |
| display::DisplayIdList destination_ids = |
| display_manager()->GetMirroringDestinationDisplayIdList(); |
| EXPECT_EQ(1U, destination_ids.size()); |
| EXPECT_EQ(first_display_id, destination_ids[0]); |
| |
| // Check the preferences. |
| const base::DictionaryValue* pref_data = |
| local_state()->GetDictionary(prefs::kDisplayMixedMirrorModeParams); |
| EXPECT_EQ(base::Int64ToString(internal_display_id), |
| pref_data->FindKey("mirroring_source_id")->GetString()); |
| const base::Value* destination_ids_value = |
| pref_data->FindKey("mirroring_destination_ids"); |
| EXPECT_EQ(1U, destination_ids_value->GetList().size()); |
| EXPECT_EQ(base::Int64ToString(first_display_id), |
| destination_ids_value->GetList()[0].GetString()); |
| |
| // Overwrite current mixed mirror mode with a new configuration. (Mirror from |
| // the first external display to the second external display) |
| dst_ids.clear(); |
| dst_ids.emplace_back(second_display_id); |
| base::Optional<display::MixedMirrorModeParams> new_mixed_params( |
| base::in_place, first_display_id, dst_ids); |
| display_manager()->SetMirrorMode(display::MirrorMode::kMixed, |
| new_mixed_params); |
| EXPECT_TRUE(display_manager()->IsInSoftwareMirrorMode()); |
| EXPECT_EQ(first_display_id, display_manager()->mirroring_source_id()); |
| destination_ids = display_manager()->GetMirroringDestinationDisplayIdList(); |
| EXPECT_EQ(1U, destination_ids.size()); |
| EXPECT_EQ(second_display_id, destination_ids[0]); |
| |
| // Check the preferences. |
| pref_data = |
| local_state()->GetDictionary(prefs::kDisplayMixedMirrorModeParams); |
| EXPECT_EQ(base::Int64ToString(first_display_id), |
| pref_data->FindKey("mirroring_source_id")->GetString()); |
| destination_ids_value = pref_data->FindKey("mirroring_destination_ids"); |
| EXPECT_EQ(1U, destination_ids_value->GetList().size()); |
| EXPECT_EQ(base::Int64ToString(second_display_id), |
| destination_ids_value->GetList()[0].GetString()); |
| |
| // Turn off mirror mode. |
| display_manager()->SetMirrorMode(display::MirrorMode::kOff, base::nullopt); |
| EXPECT_FALSE(display_manager()->IsInMirrorMode()); |
| |
| // Check the preferences. |
| pref_data = |
| local_state()->GetDictionary(prefs::kDisplayMixedMirrorModeParams); |
| EXPECT_TRUE(pref_data->empty()); |
| } |
| |
| } // namespace ash |