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