// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/display/resolution_notification_controller.h"
#include <utility>
#include "ash/display/display_change_dialog.h"
#include "ash/display/display_util.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/screen_layout_observer.h"
#include "base/functional/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/display_features.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/manager/managed_display_info.h"
#include "ui/display/util/display_util.h"
namespace ash {
struct ResolutionNotificationController::ResolutionChangeInfo {
ResolutionChangeInfo(int64_t display_id,
const display::ManagedDisplayMode& old_resolution,
const display::ManagedDisplayMode& new_resolution,
base::OnceClosure accept_callback);
ResolutionChangeInfo(const ResolutionChangeInfo&) = delete;
ResolutionChangeInfo& operator=(const ResolutionChangeInfo&) = delete;
~ResolutionChangeInfo();
// The id of the display where the resolution change happens.
const int64_t display_id;
// The resolution before the change.
display::ManagedDisplayMode old_resolution;
// The requested resolution. Note that this may be different from
// |current_resolution| which is the actual resolution set.
display::ManagedDisplayMode new_resolution;
// The actual resolution after the change.
display::ManagedDisplayMode current_resolution;
// The callback when accept is chosen.
base::OnceClosure accept_callback;
};
ResolutionNotificationController::ResolutionChangeInfo::ResolutionChangeInfo(
int64_t display_id,
const display::ManagedDisplayMode& old_resolution,
const display::ManagedDisplayMode& new_resolution,
base::OnceClosure accept_callback)
: display_id(display_id),
old_resolution(old_resolution),
new_resolution(new_resolution),
accept_callback(std::move(accept_callback)) {}
ResolutionNotificationController::ResolutionChangeInfo::
~ResolutionChangeInfo() = default;
ResolutionNotificationController::ResolutionNotificationController() {
Shell::Get()->display_manager()->AddDisplayManagerObserver(this);
}
ResolutionNotificationController::~ResolutionNotificationController() {
Shell::Get()->display_manager()->RemoveDisplayManagerObserver(this);
}
bool ResolutionNotificationController::PrepareNotificationAndSetDisplayMode(
int64_t display_id,
const display::ManagedDisplayMode& old_resolution,
const display::ManagedDisplayMode& new_resolution,
crosapi::mojom::DisplayConfigSource source,
base::OnceClosure accept_callback) {
Shell::Get()->screen_layout_observer()->SetDisplayChangedFromSettingsUI(
display_id);
display::DisplayManager* const display_manager =
Shell::Get()->display_manager();
if (source == crosapi::mojom::DisplayConfigSource::kPolicy ||
display::IsInternalDisplayId(display_id)) {
// We don't show notifications to confirm/revert the resolution change in
// the case of an internal display or policy-forced changes.
return display_manager->SetDisplayMode(display_id, new_resolution);
}
// If multiple resolution changes are invoked for the same display,
// the original resolution for the first resolution change has to be used
// instead of the specified |old_resolution|.
display::ManagedDisplayMode original_resolution;
if (change_info_ && change_info_->display_id == display_id) {
DCHECK_EQ(change_info_->new_resolution.size(), old_resolution.size());
original_resolution = change_info_->old_resolution;
}
if (change_info_ && change_info_->display_id != display_id) {
// Preparing the notification for a new resolution change of another display
// before the previous one was accepted. We decided that it's safer to
// revert the previous resolution change since the user didn't explicitly
// accept it, and we have no way of knowing for sure that it worked.
RevertResolutionChange(false /* display_was_removed */);
}
change_info_ = std::make_unique<ResolutionChangeInfo>(
display_id, old_resolution, new_resolution, std::move(accept_callback));
if (!original_resolution.size().IsEmpty()) {
change_info_->old_resolution = original_resolution;
}
if (!display_manager->SetDisplayMode(display_id, new_resolution)) {
// Discard the prepared notification data since we failed to set the new
// resolution.
change_info_.reset();
return false;
}
return true;
}
bool ResolutionNotificationController::ShouldShowDisplayChangeDialog() const {
return change_info_ && Shell::Get()->session_controller()->login_status() !=
LoginStatus::KIOSK_APP;
}
void ResolutionNotificationController::CreateOrReplaceModalDialog() {
if (confirmation_dialog_) {
confirmation_dialog_->GetWidget()->CloseNow();
}
if (!ShouldShowDisplayChangeDialog()) {
return;
}
const std::u16string display_name =
base::UTF8ToUTF16(Shell::Get()->display_manager()->GetDisplayNameForId(
change_info_->display_id));
const std::u16string actual_display_size =
base::UTF8ToUTF16(change_info_->current_resolution.size().ToString());
const std::u16string requested_display_size =
base::UTF8ToUTF16(change_info_->new_resolution.size().ToString());
std::u16string dialog_title =
l10n_util::GetStringUTF16(IDS_ASH_RESOLUTION_CHANGE_DIALOG_TITLE);
// Construct the timeout message, leaving a placeholder for the countdown
// timer so that the string does not need to be completely rebuilt every
// timer tick.
constexpr char16_t kTimeoutPlaceHolder[] = u"$1";
std::u16string timeout_message_with_placeholder;
if (display::features::IsListAllDisplayModesEnabled()) {
const std::u16string actual_refresh_rate = ConvertRefreshRateToString16(
change_info_->current_resolution.refresh_rate());
const std::u16string requested_refresh_rate = ConvertRefreshRateToString16(
change_info_->new_resolution.refresh_rate());
const bool no_fallback = actual_display_size == requested_display_size &&
actual_refresh_rate == requested_refresh_rate;
dialog_title =
no_fallback
? l10n_util::GetStringUTF16(
IDS_ASH_RESOLUTION_REFRESH_CHANGE_DIALOG_TITLE_SUCCESS)
: l10n_util::GetStringUTF16(
IDS_ASH_RESOLUTION_REFRESH_CHANGE_DIALOG_TITLE_FALLBACK);
timeout_message_with_placeholder =
no_fallback ? l10n_util::GetStringFUTF16(
IDS_ASH_RESOLUTION_REFRESH_CHANGE_DIALOG_CHANGED_NEW,
display_name, actual_display_size,
actual_refresh_rate, kTimeoutPlaceHolder)
: l10n_util::GetStringFUTF16(
IDS_ASH_RESOLUTION_REFRESH_CHANGE_DIALOG_FALLBACK_NEW,
{display_name, actual_display_size,
actual_refresh_rate, requested_display_size,
requested_refresh_rate, kTimeoutPlaceHolder},
/*offsets=*/nullptr);
} else {
timeout_message_with_placeholder =
actual_display_size == requested_display_size
? l10n_util::GetStringFUTF16(
IDS_ASH_RESOLUTION_CHANGE_DIALOG_CHANGED, display_name,
actual_display_size, kTimeoutPlaceHolder)
: l10n_util::GetStringFUTF16(
IDS_ASH_RESOLUTION_CHANGE_DIALOG_FALLBACK, display_name,
requested_display_size, actual_display_size,
kTimeoutPlaceHolder);
}
DisplayChangeDialog* dialog = new DisplayChangeDialog(
std::move(dialog_title), std::move(timeout_message_with_placeholder),
base::BindOnce(&ResolutionNotificationController::AcceptResolutionChange,
weak_factory_.GetWeakPtr()),
base::BindOnce(&ResolutionNotificationController::RevertResolutionChange,
weak_factory_.GetWeakPtr()));
confirmation_dialog_ = dialog->GetWeakPtr();
}
void ResolutionNotificationController::AcceptResolutionChange() {
if (!change_info_) {
return;
}
base::OnceClosure callback = std::move(change_info_->accept_callback);
change_info_.reset();
std::move(callback).Run();
}
void ResolutionNotificationController::RevertResolutionChange(
bool display_was_removed) {
if (!change_info_) {
return;
}
const int64_t display_id = change_info_->display_id;
display::ManagedDisplayMode old_resolution = change_info_->old_resolution;
change_info_.reset();
Shell::Get()->screen_layout_observer()->SetDisplayChangedFromSettingsUI(
display_id);
if (display_was_removed) {
// If display was removed then we are inside the stack of
// DisplayManager::UpdateDisplaysWith(), and we need to update the selected
// mode of this removed display without reentering again into
// UpdateDisplaysWith() because this can cause a crash. crbug.com/709722.
Shell::Get()->display_manager()->SetSelectedModeForDisplayId(
display_id, old_resolution);
} else {
Shell::Get()->display_manager()->SetDisplayMode(display_id, old_resolution);
}
}
void ResolutionNotificationController::OnDisplaysRemoved(
const display::Displays& removed_displays) {
for (const auto& display : removed_displays) {
if (change_info_ && change_info_->display_id == display.id()) {
if (confirmation_dialog_) {
// Use CloseWithReason rather than CloseNow to make sure the screen
// doesn't stay dimmed after the widget is closed. b/288485093.
confirmation_dialog_->GetWidget()->CloseWithReason(
views::Widget::ClosedReason::kLostFocus);
}
RevertResolutionChange(/*display_was_removed=*/true);
break;
}
}
}
void ResolutionNotificationController::OnDidApplyDisplayChanges() {
if (!change_info_) {
return;
}
display::ManagedDisplayMode mode;
if (Shell::Get()->display_manager()->GetActiveModeForDisplayId(
change_info_->display_id, &mode)) {
change_info_->current_resolution = mode;
}
CreateOrReplaceModalDialog();
}
} // namespace ash