1 // Copyright 2018 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 "chrome/browser/ui/ash/launcher/shelf_spinner_controller.h"
6
7 #include <vector>
8
9 #include "ash/public/cpp/shelf_model.h"
10 #include "base/bind.h"
11 #include "base/numerics/ranges.h"
12 #include "base/threading/thread_task_runner_handle.h"
13 #include "chrome/browser/chromeos/crostini/crostini_shelf_utils.h"
14 #include "chrome/browser/chromeos/profiles/profile_helper.h"
15 #include "chrome/browser/profiles/profile.h"
16 #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller.h"
17 #include "chrome/browser/ui/ash/launcher/shelf_spinner_item_controller.h"
18 #include "components/user_manager/user_manager.h"
19 #include "ui/gfx/canvas.h"
20 #include "ui/gfx/image/canvas_image_source.h"
21 #include "ui/gfx/image/image_skia_operations.h"
22 #include "ui/gfx/paint_throbber.h"
23
24 namespace {
25
26 constexpr int kUpdateIconIntervalMs = 40; // 40ms for 25 frames per second.
27
28 // Controls the spinner animation. See crbug.com/922977 for details.
29 constexpr base::TimeDelta kFadeInDuration =
30 base::TimeDelta::FromMilliseconds(200);
31 constexpr base::TimeDelta kFadeOutDuration =
32 base::TimeDelta::FromMilliseconds(200);
33 constexpr base::TimeDelta kMinimumShowDuration =
34 base::TimeDelta::FromMilliseconds(200);
35
36 constexpr int kSpinningGapPercent = 25;
37 constexpr color_utils::HSL kInactiveHslShift = {-1, 0, 0.25};
38 constexpr double kInactiveTransparency = 0.5;
39
40 // Returns the proportion of the duration |d| from |t1| to |t2|, where 0 means
41 // |t2| is before or at |t1| and 1 means it is |d| or further ahead.
TimeProportionSince(const base::Time & t1,const base::Time & t2,const base::TimeDelta & d)42 double TimeProportionSince(const base::Time& t1,
43 const base::Time& t2,
44 const base::TimeDelta& d) {
45 return base::ClampToRange((t2 - t1) / d, 0.0, 1.0);
46 }
47
48 } // namespace
49
50 class ShelfSpinnerController::ShelfSpinnerData {
51 public:
ShelfSpinnerData(ShelfSpinnerItemController * controller)52 explicit ShelfSpinnerData(ShelfSpinnerItemController* controller)
53 : controller_(controller),
54 creation_time_(controller->start_time()),
55 removal_time_() {}
56
57 ~ShelfSpinnerData() = default;
58
59 // Returns true if we are currently fading the spinner in. This will also
60 // return true when the spinner is animating but has finished fading in.
IsFadingIn() const61 bool IsFadingIn() const {
62 return !IsKilled() || (base::Time::Now() < removal_time_);
63 }
64
65 // Returns true if we have completed the fade-out animation.
IsFinished() const66 bool IsFinished() const {
67 return IsKilled() && base::Time::Now() >= removal_time_ + kFadeOutDuration;
68 }
69
70 // Returns true if this spinner has been killed (no matter what stage of the
71 // animation it is up to).
IsKilled() const72 bool IsKilled() const { return controller_ == nullptr; }
73
74 // Marks the spinner as completed, which begins the fade out animation
75 // either now, or at a point in the future when the minimum show duration
76 // has been met.
Kill()77 void Kill() {
78 removal_time_ =
79 std::max(base::Time::Now(), creation_time_ + kMinimumShowDuration);
80 controller_ = nullptr;
81 }
82
controller() const83 ShelfSpinnerItemController* controller() const { return controller_; }
84
85 // Get a timestamp for when the spinner was started.
creation_time() const86 base::Time creation_time() const { return creation_time_; }
87
88 // Get a timestamp for when the spinner's fade-out animation begins. This
89 // will be in the future if the spiiner was Kill()ed before the minimum show
90 // duration was reached.
removal_time() const91 base::Time removal_time() const { return removal_time_; }
92
93 private:
94 ShelfSpinnerItemController* controller_;
95 base::Time creation_time_;
96 base::Time removal_time_;
97 };
98
99 namespace {
100
101 class SpinningEffectSource : public gfx::CanvasImageSource {
102 public:
SpinningEffectSource(ShelfSpinnerController::ShelfSpinnerData data,const gfx::ImageSkia & image,bool is_pinned)103 SpinningEffectSource(ShelfSpinnerController::ShelfSpinnerData data,
104 const gfx::ImageSkia& image,
105 bool is_pinned)
106 : gfx::CanvasImageSource(image.size()),
107 data_(std::move(data)),
108 active_image_(
109 (is_pinned || !data_.IsFadingIn())
110 ? image
111 : gfx::ImageSkiaOperations::CreateTransparentImage(image, 0)),
112 inactive_image_(gfx::ImageSkiaOperations::CreateTransparentImage(
113 gfx::ImageSkiaOperations::CreateHSLShiftedImage(image,
114 kInactiveHslShift),
115 kInactiveTransparency)) {}
116
~SpinningEffectSource()117 ~SpinningEffectSource() override {}
118
119 // gfx::CanvasImageSource override.
Draw(gfx::Canvas * canvas)120 void Draw(gfx::Canvas* canvas) override {
121 base::Time now = base::Time::Now();
122 double animation_lirp = GetAnimationStage(now);
123
124 canvas->DrawImageInt(gfx::ImageSkiaOperations::CreateBlendedImage(
125 inactive_image_, active_image_, animation_lirp),
126 0, 0);
127
128 const int gap = kSpinningGapPercent * inactive_image_.width() / 100;
129 gfx::PaintThrobberSpinning(
130 canvas,
131 gfx::Rect(gap, gap, inactive_image_.width() - 2 * gap,
132 inactive_image_.height() - 2 * gap),
133 SkColorSetA(SK_ColorWHITE, 0xFF * (1.0 - std::abs(animation_lirp))),
134 now - data_.creation_time());
135 }
136
137 private:
138 // Returns a number in the range [0, 1] where:
139 // - 0 -> spinner image is completely shown.
140 // - 0.5 -> spinner image is half-way gone.
141 // - 1 -> normal image is shown.
GetAnimationStage(const base::Time & now)142 double GetAnimationStage(const base::Time& now) {
143 if (data_.IsFadingIn()) {
144 return 1.0 -
145 TimeProportionSince(data_.creation_time(), now, kFadeInDuration);
146 } else {
147 return TimeProportionSince(data_.removal_time(), now, kFadeOutDuration);
148 }
149 }
150
151 ShelfSpinnerController::ShelfSpinnerData data_;
152 const gfx::ImageSkia active_image_;
153 const gfx::ImageSkia inactive_image_;
154
155 DISALLOW_COPY_AND_ASSIGN(SpinningEffectSource);
156 };
157
158 } // namespace
159
ShelfSpinnerController(ChromeLauncherController * owner)160 ShelfSpinnerController::ShelfSpinnerController(ChromeLauncherController* owner)
161 : owner_(owner) {
162 owner->shelf_model()->AddObserver(this);
163 if (user_manager::UserManager::IsInitialized()) {
164 if (auto* active_user = user_manager::UserManager::Get()->GetActiveUser())
165 current_account_id_ = active_user->GetAccountId();
166 else
167 LOG(ERROR) << "Failed to get active user, UserManager returned null";
168 } else {
169 LOG(ERROR) << "Failed to get active user, UserManager is not initialized";
170 }
171 }
172
~ShelfSpinnerController()173 ShelfSpinnerController::~ShelfSpinnerController() {
174 owner_->shelf_model()->RemoveObserver(this);
175 }
176
MaybeApplySpinningEffect(const std::string & app_id,gfx::ImageSkia * image)177 void ShelfSpinnerController::MaybeApplySpinningEffect(const std::string& app_id,
178 gfx::ImageSkia* image) {
179 DCHECK(image);
180 auto it = app_controller_map_.find(app_id);
181 if (it == app_controller_map_.end())
182 return;
183
184 *image = gfx::ImageSkia(std::make_unique<SpinningEffectSource>(
185 it->second, *image, owner_->IsAppPinned(app_id)),
186 image->size());
187 }
188
HideSpinner(const std::string & app_id)189 void ShelfSpinnerController::HideSpinner(const std::string& app_id) {
190 if (!RemoveSpinnerFromControllerMap(app_id))
191 return;
192
193 const ash::ShelfID shelf_id(app_id);
194
195 // If the app whose spinner is being hidden is pinned, we don't want to un-pin
196 // it when we remove it from the shelf, so disable pin syncing while we update
197 // things.
198 auto pin_disabler = owner_->GetScopedPinSyncDisabler();
199 // The static_cast here is safe, because if the delegate were not a
200 // ShelfSpinnerItemController then ShelfItemDelegateChanged would have been
201 // called and we would not have reached this place.
202 auto delegate =
203 owner_->shelf_model()->RemoveItemAndTakeShelfItemDelegate(shelf_id);
204 std::unique_ptr<ShelfSpinnerItemController> cast_delegate(
205 static_cast<ShelfSpinnerItemController*>(delegate.release()));
206
207 hidden_app_controller_map_.emplace(
208 current_account_id_, std::make_pair(app_id, std::move(cast_delegate)));
209 }
210
CloseSpinner(const std::string & app_id)211 void ShelfSpinnerController::CloseSpinner(const std::string& app_id) {
212 if (!RemoveSpinnerFromControllerMap(app_id))
213 return;
214
215 owner_->CloseLauncherItem(ash::ShelfID(app_id));
216 UpdateShelfItemIcon(app_id);
217 }
218
RemoveSpinnerFromControllerMap(const std::string & app_id)219 bool ShelfSpinnerController::RemoveSpinnerFromControllerMap(
220 const std::string& app_id) {
221 AppControllerMap::const_iterator it = app_controller_map_.find(app_id);
222 if (it == app_controller_map_.end())
223 return false;
224
225 const ash::ShelfID shelf_id(app_id);
226 DCHECK_EQ(it->second.controller(),
227 it->second.IsKilled()
228 ? nullptr
229 : owner_->shelf_model()->GetShelfItemDelegate(shelf_id));
230 app_controller_map_.erase(it);
231
232 return true;
233 }
234
CloseCrostiniSpinners()235 void ShelfSpinnerController::CloseCrostiniSpinners() {
236 std::vector<std::string> app_ids_to_close;
237 const Profile* profile =
238 chromeos::ProfileHelper::Get()->GetProfileByAccountId(
239 current_account_id_);
240 for (const auto& app_id_controller_pair : app_controller_map_) {
241 if (crostini::IsCrostiniShelfAppId(profile, app_id_controller_pair.first))
242 app_ids_to_close.push_back(app_id_controller_pair.first);
243 }
244 for (const auto& app_id : app_ids_to_close)
245 CloseSpinner(app_id);
246 }
247
HasApp(const std::string & app_id) const248 bool ShelfSpinnerController::HasApp(const std::string& app_id) const {
249 auto it = app_controller_map_.find(app_id);
250 return it != app_controller_map_.end() && !it->second.IsKilled();
251 }
252
GetActiveTime(const std::string & app_id) const253 base::TimeDelta ShelfSpinnerController::GetActiveTime(
254 const std::string& app_id) const {
255 AppControllerMap::const_iterator it = app_controller_map_.find(app_id);
256 if (it == app_controller_map_.end())
257 return base::TimeDelta();
258
259 return base::Time::Now() - it->second.creation_time();
260 }
261
OwnerProfile()262 Profile* ShelfSpinnerController::OwnerProfile() {
263 return owner_->profile();
264 }
265
ShelfItemDelegateChanged(const ash::ShelfID & id,ash::ShelfItemDelegate * old_delegate,ash::ShelfItemDelegate * delegate)266 void ShelfSpinnerController::ShelfItemDelegateChanged(
267 const ash::ShelfID& id,
268 ash::ShelfItemDelegate* old_delegate,
269 ash::ShelfItemDelegate* delegate) {
270 auto it = app_controller_map_.find(id.app_id);
271 if (it != app_controller_map_.end()) {
272 it->second.Kill();
273 }
274 }
275
ActiveUserChanged(const AccountId & account_id)276 void ShelfSpinnerController::ActiveUserChanged(const AccountId& account_id) {
277 if (account_id == current_account_id_) {
278 LOG(WARNING) << "Tried switching to currently active user";
279 return;
280 }
281
282 std::vector<std::string> to_hide;
283 std::vector<
284 std::pair<std::string, std::unique_ptr<ShelfSpinnerItemController>>>
285 to_show;
286
287 for (const auto& app_id : app_controller_map_)
288 to_hide.push_back(app_id.first);
289 for (auto it = hidden_app_controller_map_.lower_bound(account_id);
290 it != hidden_app_controller_map_.upper_bound(account_id); it++) {
291 to_show.push_back(std::move(it->second));
292 }
293
294 hidden_app_controller_map_.erase(
295 hidden_app_controller_map_.lower_bound(account_id),
296 hidden_app_controller_map_.upper_bound(account_id));
297
298 for (const auto& app_id : to_hide)
299 HideSpinner(app_id);
300
301 for (auto& app_id_delegate_pair : to_show) {
302 AddSpinnerToShelf(app_id_delegate_pair.first,
303 std::move(app_id_delegate_pair.second));
304 }
305
306 current_account_id_ = account_id;
307 }
308
UpdateShelfItemIcon(const std::string & app_id)309 void ShelfSpinnerController::UpdateShelfItemIcon(const std::string& app_id) {
310 owner_->UpdateLauncherItemImage(app_id);
311 }
312
UpdateApps()313 void ShelfSpinnerController::UpdateApps() {
314 if (app_controller_map_.empty())
315 return;
316
317 RegisterNextUpdate();
318 std::vector<std::string> app_ids_to_close;
319 for (const auto& pair : app_controller_map_) {
320 UpdateShelfItemIcon(pair.first);
321 if (pair.second.IsFinished())
322 app_ids_to_close.emplace_back(pair.first);
323 }
324 for (const auto& app_id : app_ids_to_close) {
325 if (RemoveSpinnerFromControllerMap(app_id))
326 UpdateShelfItemIcon(app_id);
327 }
328 }
329
RegisterNextUpdate()330 void ShelfSpinnerController::RegisterNextUpdate() {
331 base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
332 FROM_HERE,
333 base::BindOnce(&ShelfSpinnerController::UpdateApps,
334 weak_ptr_factory_.GetWeakPtr()),
335 base::TimeDelta::FromMilliseconds(kUpdateIconIntervalMs));
336 }
337
AddSpinnerToShelf(const std::string & app_id,std::unique_ptr<ShelfSpinnerItemController> controller)338 void ShelfSpinnerController::AddSpinnerToShelf(
339 const std::string& app_id,
340 std::unique_ptr<ShelfSpinnerItemController> controller) {
341 const ash::ShelfID shelf_id(app_id);
342
343 // We should only apply the spinner controller only over non-active items.
344 const ash::ShelfItem* item = owner_->GetItem(shelf_id);
345 if (item && item->status != ash::STATUS_CLOSED)
346 return;
347
348 controller->SetHost(weak_ptr_factory_.GetWeakPtr());
349 ShelfSpinnerItemController* item_controller = controller.get();
350 if (!item) {
351 owner_->CreateAppLauncherItem(std::move(controller), ash::STATUS_RUNNING);
352 } else {
353 owner_->shelf_model()->SetShelfItemDelegate(shelf_id,
354 std::move(controller));
355 owner_->SetItemStatus(shelf_id, ash::STATUS_RUNNING);
356 }
357
358 if (app_controller_map_.empty())
359 RegisterNextUpdate();
360
361 app_controller_map_.emplace(app_id, ShelfSpinnerData(item_controller));
362 }
363