// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/ash/shelf/shelf_spinner_controller.h"
#include <algorithm>
#include <vector>
#include "ash/public/cpp/shelf_model.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/ash/guest_os/guest_os_shelf_utils.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/shelf_spinner_item_controller.h"
#include "components/user_manager/user_manager.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/image/canvas_image_source.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_throbber.h"
namespace {
constexpr int kUpdateIconIntervalMs = 40; // 40ms for 25 frames per second.
// Controls the spinner animation. See crbug.com/922977 for details.
constexpr base::TimeDelta kFadeInDuration = base::Milliseconds(200);
constexpr base::TimeDelta kFadeOutDuration = base::Milliseconds(200);
constexpr base::TimeDelta kMinimumShowDuration = base::Milliseconds(200);
constexpr int kSpinningGapPercent = 25;
constexpr color_utils::HSL kInactiveHslShift = {-1, 0, 0.25};
constexpr double kInactiveTransparency = 0.5;
// Returns the proportion of the duration |d| from |t1| to |t2|, where 0 means
// |t2| is before or at |t1| and 1 means it is |d| or further ahead.
double TimeProportionSince(const base::Time& t1,
const base::Time& t2,
const base::TimeDelta& d) {
return std::clamp((t2 - t1) / d, 0.0, 1.0);
}
} // namespace
class ShelfSpinnerController::ShelfSpinnerData {
public:
explicit ShelfSpinnerData(ShelfSpinnerItemController* controller)
: controller_(controller),
creation_time_(controller->start_time()),
removal_time_() {}
~ShelfSpinnerData() = default;
// Returns true if we are currently fading the spinner in. This will also
// return true when the spinner is animating but has finished fading in.
bool IsFadingIn() const {
return !IsKilled() || (base::Time::Now() < removal_time_);
}
// Returns true if we have completed the fade-out animation.
bool IsFinished() const {
return IsKilled() && base::Time::Now() >= removal_time_ + kFadeOutDuration;
}
// Returns true if this spinner has been killed (no matter what stage of the
// animation it is up to).
bool IsKilled() const { return controller_ == nullptr; }
// Marks the spinner as completed, which begins the fade out animation
// either now, or at a point in the future when the minimum show duration
// has been met.
void Kill() {
removal_time_ =
std::max(base::Time::Now(), creation_time_ + kMinimumShowDuration);
controller_ = nullptr;
}
ShelfSpinnerItemController* controller() const { return controller_; }
// Get a timestamp for when the spinner was started.
base::Time creation_time() const { return creation_time_; }
// Get a timestamp for when the spinner's fade-out animation begins. This
// will be in the future if the spiiner was Kill()ed before the minimum show
// duration was reached.
base::Time removal_time() const { return removal_time_; }
private:
raw_ptr<ShelfSpinnerItemController, DanglingUntriaged> controller_;
base::Time creation_time_;
base::Time removal_time_;
};
namespace {
class SpinningEffectSource : public gfx::CanvasImageSource {
public:
SpinningEffectSource(ShelfSpinnerController::ShelfSpinnerData data,
const gfx::ImageSkia& image,
bool is_pinned)
: gfx::CanvasImageSource(image.size()),
data_(std::move(data)),
active_image_(
(is_pinned || !data_.IsFadingIn())
? image
: gfx::ImageSkiaOperations::CreateTransparentImage(image, 0)),
inactive_image_(gfx::ImageSkiaOperations::CreateTransparentImage(
gfx::ImageSkiaOperations::CreateHSLShiftedImage(image,
kInactiveHslShift),
kInactiveTransparency)) {}
SpinningEffectSource(const SpinningEffectSource&) = delete;
SpinningEffectSource& operator=(const SpinningEffectSource&) = delete;
~SpinningEffectSource() override {}
// gfx::CanvasImageSource override.
void Draw(gfx::Canvas* canvas) override {
base::Time now = base::Time::Now();
double animation_lirp = GetAnimationStage(now);
canvas->DrawImageInt(gfx::ImageSkiaOperations::CreateBlendedImage(
inactive_image_, active_image_, animation_lirp),
0, 0);
const int gap = kSpinningGapPercent * inactive_image_.width() / 100;
constexpr SkColor kThrobberColor = SK_ColorWHITE;
gfx::PaintThrobberSpinning(
canvas,
gfx::Rect(gap, gap, inactive_image_.width() - 2 * gap,
inactive_image_.height() - 2 * gap),
SkColorSetA(kThrobberColor, SkColorGetA(kThrobberColor) *
(1.0 - std::abs(animation_lirp))),
now - data_.creation_time());
}
private:
// Returns a number in the range [0, 1] where:
// - 0 -> spinner image is completely shown.
// - 0.5 -> spinner image is half-way gone.
// - 1 -> normal image is shown.
double GetAnimationStage(const base::Time& now) {
if (data_.IsFadingIn()) {
return 1.0 -
TimeProportionSince(data_.creation_time(), now, kFadeInDuration);
}
return TimeProportionSince(data_.removal_time(), now, kFadeOutDuration);
}
ShelfSpinnerController::ShelfSpinnerData data_;
const gfx::ImageSkia active_image_;
const gfx::ImageSkia inactive_image_;
};
} // namespace
ShelfSpinnerController::ShelfSpinnerController(ChromeShelfController* owner)
: owner_(owner) {
owner->shelf_model()->AddObserver(this);
if (user_manager::UserManager::IsInitialized()) {
if (auto* active_user = user_manager::UserManager::Get()->GetActiveUser())
current_account_id_ = active_user->GetAccountId();
else
LOG(ERROR) << "Failed to get active user, UserManager returned null";
} else {
LOG(ERROR) << "Failed to get active user, UserManager is not initialized";
}
}
ShelfSpinnerController::~ShelfSpinnerController() {
owner_->shelf_model()->RemoveObserver(this);
}
void ShelfSpinnerController::MaybeApplySpinningEffect(const std::string& app_id,
gfx::ImageSkia* image) {
DCHECK(image);
auto it = app_controller_map_.find(app_id);
if (it == app_controller_map_.end())
return;
*image = gfx::ImageSkia(std::make_unique<SpinningEffectSource>(
it->second, *image, owner_->IsAppPinned(app_id)),
image->size());
}
void ShelfSpinnerController::HideSpinner(const std::string& app_id) {
if (!RemoveSpinnerFromControllerMap(app_id))
return;
const ash::ShelfID shelf_id(app_id);
// If the app whose spinner is being hidden is pinned, we don't want to un-pin
// it when we remove it from the shelf, so disable pin syncing while we update
// things.
auto pin_disabler = owner_->GetScopedPinSyncDisabler();
// The static_cast here is safe, because if the delegate were not a
// ShelfSpinnerItemController then ShelfItemDelegateChanged would have been
// called and we would not have reached this place.
auto delegate =
owner_->shelf_model()->RemoveItemAndTakeShelfItemDelegate(shelf_id);
std::unique_ptr<ShelfSpinnerItemController> cast_delegate(
static_cast<ShelfSpinnerItemController*>(delegate.release()));
hidden_app_controller_map_.emplace(
current_account_id_, std::make_pair(app_id, std::move(cast_delegate)));
}
void ShelfSpinnerController::CloseSpinner(const std::string& app_id) {
if (!RemoveSpinnerFromControllerMap(app_id))
return;
owner_->ReplaceWithAppShortcutOrRemove(ash::ShelfID(app_id));
UpdateShelfItemIcon(app_id);
}
bool ShelfSpinnerController::RemoveSpinnerFromControllerMap(
const std::string& app_id) {
AppControllerMap::const_iterator it = app_controller_map_.find(app_id);
if (it == app_controller_map_.end())
return false;
const ash::ShelfID shelf_id(app_id);
DCHECK_EQ(it->second.controller(),
it->second.IsKilled()
? nullptr
: owner_->shelf_model()->GetShelfItemDelegate(shelf_id));
app_controller_map_.erase(it);
return true;
}
void ShelfSpinnerController::CloseCrostiniSpinners() {
std::vector<std::string> app_ids_to_close;
const Profile* profile =
ash::ProfileHelper::Get()->GetProfileByAccountId(current_account_id_);
for (const auto& app_id_controller_pair : app_controller_map_) {
if (guest_os::IsCrostiniShelfAppId(profile, app_id_controller_pair.first)) {
app_ids_to_close.push_back(app_id_controller_pair.first);
}
}
for (const auto& app_id : app_ids_to_close)
CloseSpinner(app_id);
}
bool ShelfSpinnerController::HasApp(const std::string& app_id) const {
auto it = app_controller_map_.find(app_id);
return it != app_controller_map_.end() && !it->second.IsKilled();
}
base::TimeDelta ShelfSpinnerController::GetActiveTime(
const std::string& app_id) const {
AppControllerMap::const_iterator it = app_controller_map_.find(app_id);
if (it == app_controller_map_.end())
return base::TimeDelta();
return base::Time::Now() - it->second.creation_time();
}
Profile* ShelfSpinnerController::OwnerProfile() {
return owner_->profile();
}
void ShelfSpinnerController::ShelfItemDelegateChanged(
const ash::ShelfID& id,
ash::ShelfItemDelegate* old_delegate,
ash::ShelfItemDelegate* delegate) {
auto it = app_controller_map_.find(id.app_id);
if (it != app_controller_map_.end()) {
it->second.Kill();
}
}
void ShelfSpinnerController::ActiveUserChanged(const AccountId& account_id) {
if (account_id == current_account_id_) {
LOG(WARNING) << "Tried switching to currently active user";
return;
}
std::vector<std::string> to_hide;
std::vector<
std::pair<std::string, std::unique_ptr<ShelfSpinnerItemController>>>
to_show;
for (const auto& app_id : app_controller_map_)
to_hide.push_back(app_id.first);
for (auto it = hidden_app_controller_map_.lower_bound(account_id);
it != hidden_app_controller_map_.upper_bound(account_id); it++) {
to_show.push_back(std::move(it->second));
}
hidden_app_controller_map_.erase(
hidden_app_controller_map_.lower_bound(account_id),
hidden_app_controller_map_.upper_bound(account_id));
for (const auto& app_id : to_hide)
HideSpinner(app_id);
for (auto& app_id_delegate_pair : to_show) {
AddSpinnerToShelf(app_id_delegate_pair.first,
std::move(app_id_delegate_pair.second));
}
current_account_id_ = account_id;
}
void ShelfSpinnerController::UpdateShelfItemIcon(const std::string& app_id) {
owner_->UpdateItemImage(app_id);
}
void ShelfSpinnerController::UpdateApps() {
if (app_controller_map_.empty())
return;
RegisterNextUpdate();
std::vector<std::string> app_ids_to_close;
for (const auto& pair : app_controller_map_) {
UpdateShelfItemIcon(pair.first);
if (pair.second.IsFinished())
app_ids_to_close.emplace_back(pair.first);
}
for (const auto& app_id : app_ids_to_close) {
if (RemoveSpinnerFromControllerMap(app_id))
UpdateShelfItemIcon(app_id);
}
}
void ShelfSpinnerController::RegisterNextUpdate() {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ShelfSpinnerController::UpdateApps,
weak_ptr_factory_.GetWeakPtr()),
base::Milliseconds(kUpdateIconIntervalMs));
}
void ShelfSpinnerController::AddSpinnerToShelf(
const std::string& app_id,
std::unique_ptr<ShelfSpinnerItemController> controller) {
const ash::ShelfID shelf_id(app_id);
// We should only apply the spinner controller only over non-active items.
const ash::ShelfItem* item = owner_->GetItem(shelf_id);
if (item && item->status != ash::STATUS_CLOSED)
return;
controller->SetHost(weak_ptr_factory_.GetWeakPtr());
ShelfSpinnerItemController* item_controller = controller.get();
if (!item) {
owner_->CreateAppItem(std::move(controller), ash::STATUS_RUNNING,
/*pinned=*/false);
} else {
owner_->shelf_model()->ReplaceShelfItemDelegate(shelf_id,
std::move(controller));
owner_->SetItemStatus(shelf_id, ash::STATUS_RUNNING);
}
if (app_controller_map_.empty())
RegisterNextUpdate();
app_controller_map_.emplace(app_id, ShelfSpinnerData(item_controller));
}