1// Copyright (c) 2012 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 "remoting/host/resizing_host_observer.h"
6
7#include <list>
8
9#include "base/bind.h"
10#include "base/logging.h"
11#include "base/message_loop/message_loop.h"
12#include "remoting/host/desktop_resizer.h"
13#include "remoting/host/screen_resolution.h"
14
15namespace remoting {
16namespace {
17
18// Minimum amount of time to wait between desktop resizes. Note that this
19// constant is duplicated by the ResizingHostObserverTest.RateLimited
20// unit-test and must be kept in sync.
21const int kMinimumResizeIntervalMs = 1000;
22
23class CandidateResolution {
24 public:
25  CandidateResolution(const ScreenResolution& candidate,
26                      const ScreenResolution& preferred)
27      : resolution_(candidate) {
28    // Protect against division by zero.
29    CHECK(!candidate.IsEmpty());
30    DCHECK(!preferred.IsEmpty());
31
32    // The client scale factor is the smaller of the candidate:preferred ratios
33    // for width and height.
34    if ((candidate.dimensions().width() > preferred.dimensions().width()) ||
35        (candidate.dimensions().height() > preferred.dimensions().height())) {
36      const float width_ratio =
37          static_cast<float>(preferred.dimensions().width()) /
38          candidate.dimensions().width();
39      const float height_ratio =
40          static_cast<float>(preferred.dimensions().height()) /
41          candidate.dimensions().height();
42      client_scale_factor_ = std::min(width_ratio, height_ratio);
43    } else {
44      // Since clients do not scale up, 1.0 is the maximum.
45      client_scale_factor_ = 1.0;
46    }
47
48    // The aspect ratio "goodness" is defined as being the ratio of the smaller
49    // of the two aspect ratios (candidate and preferred) to the larger. The
50    // best aspect ratio is the one that most closely matches the preferred
51    // aspect ratio (in other words, the ideal aspect ratio "goodness" is 1.0).
52    // By keeping the values < 1.0, it allows ratios that differ in opposite
53    // directions to be compared numerically.
54    float candidate_aspect_ratio =
55        static_cast<float>(candidate.dimensions().width()) /
56        candidate.dimensions().height();
57    float preferred_aspect_ratio =
58        static_cast<float>(preferred.dimensions().width()) /
59        preferred.dimensions().height();
60    if (candidate_aspect_ratio > preferred_aspect_ratio) {
61      aspect_ratio_goodness_ = preferred_aspect_ratio / candidate_aspect_ratio;
62    } else {
63      aspect_ratio_goodness_ = candidate_aspect_ratio / preferred_aspect_ratio;
64    }
65  }
66
67  const ScreenResolution& resolution() const { return resolution_; }
68  float client_scale_factor() const { return client_scale_factor_; }
69  float aspect_ratio_goodness() const { return aspect_ratio_goodness_; }
70  int64 area() const {
71    return static_cast<int64>(resolution_.dimensions().width()) *
72        resolution_.dimensions().height();
73  }
74
75  // TODO(jamiewalch): Also compare the DPI: http://crbug.com/172405
76  bool IsBetterThan(const CandidateResolution& other) const {
77    // If either resolution would require down-scaling, prefer the one that
78    // down-scales the least (since the client scale factor is at most 1.0,
79    // this does not differentiate between resolutions that don't require
80    // down-scaling).
81    if (client_scale_factor() < other.client_scale_factor()) {
82      return false;
83    } else if (client_scale_factor() > other.client_scale_factor()) {
84      return true;
85    }
86
87    // If the scale factors are the same, pick the resolution with the largest
88    // area.
89    if (area() < other.area()) {
90      return false;
91    } else if (area() > other.area()) {
92      return true;
93    }
94
95    // If the areas are equal, pick the resolution with the "best" aspect ratio.
96    if (aspect_ratio_goodness() < other.aspect_ratio_goodness()) {
97      return false;
98    } else if (aspect_ratio_goodness() > other.aspect_ratio_goodness()) {
99      return true;
100    }
101
102    // All else being equal (for example, comparing 640x480 to 480x640 w.r.t.
103    // 640x640), just pick the widest, since desktop UIs are typically designed
104    // for landscape aspect ratios.
105    return resolution().dimensions().width() >
106        other.resolution().dimensions().width();
107  }
108
109 private:
110  float client_scale_factor_;
111  float aspect_ratio_goodness_;
112  ScreenResolution resolution_;
113};
114
115}  // namespace
116
117ResizingHostObserver::ResizingHostObserver(
118    scoped_ptr<DesktopResizer> desktop_resizer)
119    : desktop_resizer_(desktop_resizer.Pass()),
120      now_function_(base::Bind(base::Time::Now)),
121      weak_factory_(this) {
122}
123
124ResizingHostObserver::~ResizingHostObserver() {
125  if (!original_resolution_.IsEmpty())
126    desktop_resizer_->RestoreResolution(original_resolution_);
127}
128
129void ResizingHostObserver::SetScreenResolution(
130    const ScreenResolution& resolution) {
131  // Get the current time. This function is called exactly once for each call
132  // to SetScreenResolution to simplify the implementation of unit-tests.
133  base::Time now = now_function_.Run();
134
135  if (resolution.IsEmpty())
136    return;
137
138  // Resizing the desktop too often is probably not a good idea, so apply a
139  // simple rate-limiting scheme.
140  base::TimeDelta minimum_resize_interval =
141      base::TimeDelta::FromMilliseconds(kMinimumResizeIntervalMs);
142  base::Time next_allowed_resize =
143      previous_resize_time_ + minimum_resize_interval;
144
145  if (now < next_allowed_resize) {
146    deferred_resize_timer_.Start(
147        FROM_HERE,
148        next_allowed_resize - now,
149        base::Bind(&ResizingHostObserver::SetScreenResolution,
150                   weak_factory_.GetWeakPtr(), resolution));
151    return;
152  }
153
154  // If the implementation returns any resolutions, pick the best one according
155  // to the algorithm described in CandidateResolution::IsBetterThen.
156  std::list<ScreenResolution> resolutions =
157      desktop_resizer_->GetSupportedResolutions(resolution);
158  if (resolutions.empty())
159    return;
160  CandidateResolution best_candidate(resolutions.front(), resolution);
161  for (std::list<ScreenResolution>::const_iterator i = ++resolutions.begin();
162       i != resolutions.end(); ++i) {
163    CandidateResolution candidate(*i, resolution);
164    if (candidate.IsBetterThan(best_candidate)) {
165      best_candidate = candidate;
166    }
167  }
168  ScreenResolution current_resolution =
169      desktop_resizer_->GetCurrentResolution();
170
171  if (!best_candidate.resolution().Equals(current_resolution)) {
172    if (original_resolution_.IsEmpty())
173      original_resolution_ = current_resolution;
174    desktop_resizer_->SetResolution(best_candidate.resolution());
175  }
176
177  // Update the time of last resize to allow it to be rate-limited.
178  previous_resize_time_ = now;
179}
180
181void ResizingHostObserver::SetNowFunctionForTesting(
182    const base::Callback<base::Time(void)>& now_function) {
183  now_function_ = now_function;
184}
185
186}  // namespace remoting
187