1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ash/display/resolution_notification_controller.h"
6 
7 #include <utility>
8 
9 #include "ash/display/display_change_dialog.h"
10 #include "ash/display/display_util.h"
11 #include "ash/public/cpp/ash_features.h"
12 #include "ash/public/cpp/notification_utils.h"
13 #include "ash/resources/vector_icons/vector_icons.h"
14 #include "ash/session/session_controller_impl.h"
15 #include "ash/shell.h"
16 #include "ash/strings/grit/ash_strings.h"
17 #include "ash/system/screen_layout_observer.h"
18 #include "base/strings/utf_string_conversions.h"
19 #include "ui/base/l10n/l10n_util.h"
20 #include "ui/display/display.h"
21 #include "ui/display/display_features.h"
22 #include "ui/display/manager/display_manager.h"
23 #include "ui/display/manager/managed_display_info.h"
24 #include "ui/display/screen.h"
25 
26 namespace ash {
27 
28 struct ResolutionNotificationController::ResolutionChangeInfo {
29   ResolutionChangeInfo(int64_t display_id,
30                        const display::ManagedDisplayMode& old_resolution,
31                        const display::ManagedDisplayMode& new_resolution,
32                        base::OnceClosure accept_callback);
33   ~ResolutionChangeInfo();
34 
35   // The id of the display where the resolution change happens.
36   const int64_t display_id;
37 
38   // The resolution before the change.
39   display::ManagedDisplayMode old_resolution;
40 
41   // The requested resolution. Note that this may be different from
42   // |current_resolution| which is the actual resolution set.
43   display::ManagedDisplayMode new_resolution;
44 
45   // The actual resolution after the change.
46   display::ManagedDisplayMode current_resolution;
47 
48   // The callback when accept is chosen.
49   base::OnceClosure accept_callback;
50 
51  private:
52   DISALLOW_COPY_AND_ASSIGN(ResolutionChangeInfo);
53 };
54 
ResolutionChangeInfo(int64_t display_id,const display::ManagedDisplayMode & old_resolution,const display::ManagedDisplayMode & new_resolution,base::OnceClosure accept_callback)55 ResolutionNotificationController::ResolutionChangeInfo::ResolutionChangeInfo(
56     int64_t display_id,
57     const display::ManagedDisplayMode& old_resolution,
58     const display::ManagedDisplayMode& new_resolution,
59     base::OnceClosure accept_callback)
60     : display_id(display_id),
61       old_resolution(old_resolution),
62       new_resolution(new_resolution),
63       accept_callback(std::move(accept_callback)) {}
64 
65 ResolutionNotificationController::ResolutionChangeInfo::
66     ~ResolutionChangeInfo() = default;
67 
ResolutionNotificationController()68 ResolutionNotificationController::ResolutionNotificationController() {
69   Shell::Get()->window_tree_host_manager()->AddObserver(this);
70   display::Screen::GetScreen()->AddObserver(this);
71 }
72 
~ResolutionNotificationController()73 ResolutionNotificationController::~ResolutionNotificationController() {
74   Shell::Get()->window_tree_host_manager()->RemoveObserver(this);
75   display::Screen::GetScreen()->RemoveObserver(this);
76 }
77 
PrepareNotificationAndSetDisplayMode(int64_t display_id,const display::ManagedDisplayMode & old_resolution,const display::ManagedDisplayMode & new_resolution,mojom::DisplayConfigSource source,base::OnceClosure accept_callback)78 bool ResolutionNotificationController::PrepareNotificationAndSetDisplayMode(
79     int64_t display_id,
80     const display::ManagedDisplayMode& old_resolution,
81     const display::ManagedDisplayMode& new_resolution,
82     mojom::DisplayConfigSource source,
83     base::OnceClosure accept_callback) {
84   Shell::Get()->screen_layout_observer()->SetDisplayChangedFromSettingsUI(
85       display_id);
86   display::DisplayManager* const display_manager =
87       Shell::Get()->display_manager();
88   if (source == mojom::DisplayConfigSource::kPolicy ||
89       display::Display::IsInternalDisplayId(display_id)) {
90     // We don't show notifications to confirm/revert the resolution change in
91     // the case of an internal display or policy-forced changes.
92     return display_manager->SetDisplayMode(display_id, new_resolution);
93   }
94 
95   // If multiple resolution changes are invoked for the same display,
96   // the original resolution for the first resolution change has to be used
97   // instead of the specified |old_resolution|.
98   display::ManagedDisplayMode original_resolution;
99   if (change_info_ && change_info_->display_id == display_id) {
100     DCHECK_EQ(change_info_->new_resolution.size(), old_resolution.size());
101     original_resolution = change_info_->old_resolution;
102   }
103 
104   if (change_info_ && change_info_->display_id != display_id) {
105     // Preparing the notification for a new resolution change of another display
106     // before the previous one was accepted. We decided that it's safer to
107     // revert the previous resolution change since the user didn't explicitly
108     // accept it, and we have no way of knowing for sure that it worked.
109     RevertResolutionChange(false /* display_was_removed */);
110   }
111 
112   change_info_ = std::make_unique<ResolutionChangeInfo>(
113       display_id, old_resolution, new_resolution, std::move(accept_callback));
114   if (!original_resolution.size().IsEmpty())
115     change_info_->old_resolution = original_resolution;
116 
117   if (!display_manager->SetDisplayMode(display_id, new_resolution)) {
118     // Discard the prepared notification data since we failed to set the new
119     // resolution.
120     change_info_.reset();
121     return false;
122   }
123 
124   return true;
125 }
126 
CreateOrReplaceModalDialog()127 void ResolutionNotificationController::CreateOrReplaceModalDialog() {
128   if (confirmation_dialog_)
129     confirmation_dialog_->GetWidget()->CloseNow();
130 
131   if (!change_info_)
132     return;
133 
134   const base::string16 display_name =
135       base::UTF8ToUTF16(Shell::Get()->display_manager()->GetDisplayNameForId(
136           change_info_->display_id));
137   const base::string16 actual_display_size =
138       base::UTF8ToUTF16(change_info_->current_resolution.size().ToString());
139   const base::string16 requested_display_size =
140       base::UTF8ToUTF16(change_info_->new_resolution.size().ToString());
141 
142   base::string16 dialog_title =
143       l10n_util::GetStringUTF16(IDS_ASH_RESOLUTION_CHANGE_DIALOG_TITLE);
144 
145   // Construct the timeout message, leaving a placeholder for the countdown
146   // timer so that the string does not need to be completely rebuilt every
147   // timer tick.
148   constexpr char kTimeoutPlaceHolder[] = "$1";
149 
150   base::string16 timeout_message_with_placeholder;
151   if (display::features::IsListAllDisplayModesEnabled()) {
152     dialog_title = l10n_util::GetStringUTF16(
153         IDS_ASH_RESOLUTION_REFRESH_CHANGE_DIALOG_TITLE);
154 
155     const base::string16 actual_refresh_rate = ConvertRefreshRateToString16(
156         change_info_->current_resolution.refresh_rate());
157     const base::string16 requested_refresh_rate = ConvertRefreshRateToString16(
158         change_info_->new_resolution.refresh_rate());
159 
160     const bool no_fallback = actual_display_size == requested_display_size &&
161                              actual_refresh_rate == requested_refresh_rate;
162 
163     timeout_message_with_placeholder =
164         no_fallback
165             ? l10n_util::GetStringFUTF16(
166                   IDS_ASH_RESOLUTION_REFRESH_CHANGE_DIALOG_CHANGED,
167                   display_name, actual_display_size, actual_refresh_rate,
168                   base::UTF8ToUTF16(kTimeoutPlaceHolder))
169             : l10n_util::GetStringFUTF16(
170                   IDS_ASH_RESOLUTION_REFRESH_CHANGE_DIALOG_FALLBACK,
171                   {display_name, requested_display_size, requested_refresh_rate,
172                    actual_display_size, actual_refresh_rate,
173                    base::UTF8ToUTF16(kTimeoutPlaceHolder)},
174                   /*offsets=*/nullptr);
175 
176   } else {
177     timeout_message_with_placeholder =
178         actual_display_size == requested_display_size
179             ? l10n_util::GetStringFUTF16(
180                   IDS_ASH_RESOLUTION_CHANGE_DIALOG_CHANGED, display_name,
181                   actual_display_size, base::UTF8ToUTF16(kTimeoutPlaceHolder))
182             : l10n_util::GetStringFUTF16(
183                   IDS_ASH_RESOLUTION_CHANGE_DIALOG_FALLBACK, display_name,
184                   requested_display_size, actual_display_size,
185                   base::UTF8ToUTF16(kTimeoutPlaceHolder));
186   }
187 
188   DisplayChangeDialog* dialog = new DisplayChangeDialog(
189       std::move(dialog_title), std::move(timeout_message_with_placeholder),
190       base::BindOnce(&ResolutionNotificationController::AcceptResolutionChange,
191                      weak_factory_.GetWeakPtr()),
192       base::BindOnce(&ResolutionNotificationController::RevertResolutionChange,
193                      weak_factory_.GetWeakPtr()));
194   confirmation_dialog_ = dialog->GetWeakPtr();
195 }
196 
AcceptResolutionChange()197 void ResolutionNotificationController::AcceptResolutionChange() {
198   if (!change_info_)
199     return;
200   base::OnceClosure callback = std::move(change_info_->accept_callback);
201   change_info_.reset();
202   std::move(callback).Run();
203 }
204 
RevertResolutionChange(bool display_was_removed)205 void ResolutionNotificationController::RevertResolutionChange(
206     bool display_was_removed) {
207   if (!change_info_)
208     return;
209   const int64_t display_id = change_info_->display_id;
210   display::ManagedDisplayMode old_resolution = change_info_->old_resolution;
211   change_info_.reset();
212   Shell::Get()->screen_layout_observer()->SetDisplayChangedFromSettingsUI(
213       display_id);
214   if (display_was_removed) {
215     // If display was removed then we are inside the stack of
216     // DisplayManager::UpdateDisplaysWith(), and we need to update the selected
217     // mode of this removed display without reentering again into
218     // UpdateDisplaysWith() because this can cause a crash. crbug.com/709722.
219     Shell::Get()->display_manager()->SetSelectedModeForDisplayId(
220         display_id, old_resolution);
221   } else {
222     Shell::Get()->display_manager()->SetDisplayMode(display_id, old_resolution);
223   }
224 }
225 
OnDisplayRemoved(const display::Display & old_display)226 void ResolutionNotificationController::OnDisplayRemoved(
227     const display::Display& old_display) {
228   if (change_info_ && change_info_->display_id == old_display.id()) {
229     if (confirmation_dialog_)
230       confirmation_dialog_->GetWidget()->CloseNow();
231     RevertResolutionChange(true /* display_was_removed */);
232   }
233 }
234 
OnDisplayConfigurationChanged()235 void ResolutionNotificationController::OnDisplayConfigurationChanged() {
236   if (!change_info_)
237     return;
238 
239   display::ManagedDisplayMode mode;
240   if (Shell::Get()->display_manager()->GetActiveModeForDisplayId(
241           change_info_->display_id, &mode)) {
242     change_info_->current_resolution = mode;
243   }
244 
245   CreateOrReplaceModalDialog();
246 }
247 
248 
249 }  // namespace ash
250