chromium/ash/system/holding_space/holding_space_animation_registry.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/system/holding_space/holding_space_animation_registry.h"

#include <map>
#include <memory>
#include <set>
#include <vector>

#include "ash/public/cpp/holding_space/holding_space_controller.h"
#include "ash/public/cpp/holding_space/holding_space_controller_observer.h"
#include "ash/public/cpp/holding_space/holding_space_item_updated_fields.h"
#include "ash/public/cpp/holding_space/holding_space_model.h"
#include "ash/public/cpp/holding_space/holding_space_model_observer.h"
#include "ash/shell.h"
#include "ash/system/progress_indicator/progress_icon_animation.h"
#include "ash/system/progress_indicator/progress_ring_animation.h"
#include "base/containers/contains.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "base/ranges/algorithm.h"
#include "base/task/sequenced_task_runner.h"

namespace ash {
namespace {

// Helpers ---------------------------------------------------------------------

// Returns the owner for the singleton `HoldingSpaceAnimationRegistry` instance.
std::unique_ptr<HoldingSpaceAnimationRegistry>& GetInstanceOwner() {
  static base::NoDestructor<std::unique_ptr<HoldingSpaceAnimationRegistry>>
      instance_owner;
  return *instance_owner;
}

}  // namespace

// HoldingSpaceAnimationRegistry::ProgressIndicatorAnimationDelegate -----------

// The delegate of `HoldingSpaceAnimationRegistry` responsible for creating and
// curating progress indicator animations based on holding space model state.
class HoldingSpaceAnimationRegistry::ProgressIndicatorAnimationDelegate
    : public HoldingSpaceControllerObserver,
      public HoldingSpaceModelObserver {
 public:
  ProgressIndicatorAnimationDelegate(
      ProgressIndicatorAnimationRegistry* registry,
      HoldingSpaceController* controller)
      : registry_(registry), controller_(controller) {
    controller_observation_.Observe(controller_.get());
    if (controller_->model())
      OnHoldingSpaceModelAttached(controller_->model());
  }

  ProgressIndicatorAnimationDelegate(
      const ProgressIndicatorAnimationDelegate&) = delete;
  ProgressIndicatorAnimationDelegate& operator=(
      const ProgressIndicatorAnimationDelegate&) = delete;
  ~ProgressIndicatorAnimationDelegate() override = default;

 private:
  // HoldingSpaceControllerObserver:
  void OnHoldingSpaceModelAttached(HoldingSpaceModel* model) override {
    model_ = model;
    model_observation_.Observe(model_.get());
    UpdateAnimations(/*for_removal=*/false);
  }

  void OnHoldingSpaceModelDetached(HoldingSpaceModel* model) override {
    model_ = nullptr;
    model_observation_.Reset();
    UpdateAnimations(/*for_removal=*/false);
  }

  // HoldingSpaceModelObserver:
  void OnHoldingSpaceItemsAdded(
      const std::vector<const HoldingSpaceItem*>& items) override {
    UpdateAnimations(/*for_removal=*/false);
  }

  void OnHoldingSpaceItemsRemoved(
      const std::vector<const HoldingSpaceItem*>& items) override {
    // The removal of `items` can be safely ignored if none were in progress.
    const bool removed_in_progress_item =
        base::ranges::any_of(items, [](const HoldingSpaceItem* item) {
          return item->IsInitialized() && !item->progress().IsComplete();
        });
    if (removed_in_progress_item)
      UpdateAnimations(/*for_removal=*/true);
  }

  void OnHoldingSpaceItemInitialized(const HoldingSpaceItem* item) override {
    UpdateAnimations(/*for_removal=*/false);
  }

  void OnHoldingSpaceItemUpdated(
      const HoldingSpaceItem* item,
      const HoldingSpaceItemUpdatedFields& updated_fields) override {
    // The `item` update can be safely ignored if progress has not been updated.
    if (!updated_fields.previous_progress) {
      return;
    }

    // If `item` has just progressed to completion, ensure that a pulse
    // animation is created and started.
    if (item->progress().IsComplete()) {
      EnsureRingAnimationOfTypeForKey(AsAnimationKey(item),
                                      ProgressRingAnimation::Type::kPulse);
    }

    UpdateAnimations(/*for_removal=*/false);
  }

  // Erases the ring animation for the specified `key` if it is not of the
  // desired `type`, notifying any animation changed callbacks.
  void EraseRingAnimationIfNotOfTypeForKey(AnimationKey key,
                                           ProgressRingAnimation::Type type) {
    auto* ring_animation = registry_->GetProgressRingAnimationForKey(key);
    if (ring_animation && ring_animation->type() != type)
      registry_->SetProgressRingAnimationForKey(key, nullptr);
  }

  // Ensures that the icon animation for the specified `key` exists. If
  // necessary, a new animation is created and started, notifying any animation
  // changed callbacks.
  void EnsureIconAnimationForKey(AnimationKey key) {
    if (registry_->GetProgressIconAnimationForKey(key))
      return;

    auto* animation = registry_->SetProgressIconAnimationForKey(
        key, ProgressIconAnimation::Create());

    // Only `Start()` the `animation` if it is associated with the holding space
    // `controller_`. In all other cases, the `animation` is associated with a
    // holding space item and will be started after the associated holding space
    // tray item preview has had the opportunity to animate in.
    if (key == AsAnimationKey(controller_)) {
      animation->Start();
    }
  }

  // Ensures that the ring animation for the specified `key` is of the desired
  // `type`. If necessary, a new animation is created and started, notifying any
  // animation changed callbacks.
  void EnsureRingAnimationOfTypeForKey(AnimationKey key,
                                       ProgressRingAnimation::Type type) {
    auto* ring_animation = registry_->GetProgressRingAnimationForKey(key);
    if (ring_animation && ring_animation->type() == type)
      return;

    auto animation = ProgressRingAnimation::CreateOfType(type);
    animation->AddUnsafeAnimationUpdatedCallback(base::BindRepeating(
        &ProgressIndicatorAnimationDelegate::OnRingAnimationUpdatedForKey,
        base::Unretained(this), key, animation.get()));

    registry_->SetProgressRingAnimationForKey(key, std::move(animation))
        ->Start();
  }

  // Updates animation state for the current `model_` state. If `for_removal` is
  // `true`, the update was triggered by holding space item removal.
  void UpdateAnimations(bool for_removal) {
    // If no `model_` is currently attached, there should be no animations.
    // Animations will be updated if and when a `model_` is attached.
    if (model_ == nullptr) {
      cumulative_progress_ = HoldingSpaceProgress();
      registry_->EraseAllAnimations();
      return;
    }

    const auto controller_key = AsAnimationKey(controller_);

    // Clean up all animations associated with holding space items that are no
    // longer present in the attached `model_`.
    registry_->EraseAllAnimationsForKeyIf([&](AnimationKey key) {
      return key != controller_key &&
             !base::Contains(model_->items(), key,
                             [](const std::unique_ptr<HoldingSpaceItem>& item) {
                               return AsAnimationKey(item.get());
                             });
    });

    HoldingSpaceProgress last_cumulative_progress = cumulative_progress_;
    cumulative_progress_ = HoldingSpaceProgress();

    // Iterate over each holding space item in the attached `model_`.
    for (const auto& item : model_->items()) {
      const auto item_key = AsAnimationKey(item.get());

      // If an `item` is not initialized or is not visibly in-progress, it
      // shouldn't contribute to `cumulative_progress_` nor have an animation.
      if (!item->IsInitialized() || item->progress().IsHidden()) {
        registry_->EraseAllAnimationsForKey(item_key);
        continue;
      }

      // If the `item` is complete, it should be allowed to continue a pulse
      // animation if one was previously created and started. This would only
      // have happened in response to the `item` transitioning to completion at
      // runtime, as items that are already complete on creation are not
      // animated. Any other type of animation should be cleared. Note that a
      // completed `item` does not contribute to `cumulative_progress_`.
      if (item->progress().IsComplete()) {
        registry_->SetProgressIconAnimationForKey(item_key, nullptr);
        EraseRingAnimationIfNotOfTypeForKey(
            item_key, ProgressRingAnimation::Type::kPulse);
        continue;
      }

      cumulative_progress_ += item->progress();

      // Because the `item` is in-progress, an icon animation should be
      // associated with it (if one does not already exist).
      EnsureIconAnimationForKey(item_key);

      // If the `item` is in an indeterminate state, an indeterminate animation
      // should be associated with it (if one does not already exist).
      if (item->progress().IsIndeterminate()) {
        EnsureRingAnimationOfTypeForKey(
            item_key, ProgressRingAnimation::Type::kIndeterminate);
        continue;
      }

      // If `item` is not in an indeterminate state, it should not have an
      // associated ring animation.
      registry_->SetProgressRingAnimationForKey(item_key, nullptr);
    }

    if (cumulative_progress_.IsComplete()) {
      // Because `cumulative_progress_` is complete, the `controller_` should
      // not have an associated icon animation.
      registry_->SetProgressIconAnimationForKey(controller_key, nullptr);

      if (!last_cumulative_progress.IsComplete()) {
        if (for_removal) {
          // If `cumulative_progress_` has just become complete as a result of
          // one or more holding space items being removed, the `controller_`
          // should not have an associated ring animation.
          registry_->SetProgressRingAnimationForKey(controller_key, nullptr);
        } else {
          // If `cumulative_progress_` has just become complete and is *not* due
          // to the removal of one or more holding space items, ensure that a
          // pulse animation is created and started.
          EnsureRingAnimationOfTypeForKey(controller_key,
                                          ProgressRingAnimation::Type::kPulse);
        }
      } else {
        // If `cumulative_progress_` was already complete, it should be allowed
        // to continue a pulse animation if one was previously created and
        // started. Any other type of ring animation should be cleared.
        EraseRingAnimationIfNotOfTypeForKey(
            controller_key, ProgressRingAnimation::Type::kPulse);
      }
      return;
    }

    // Because `cumulative_progress_` is in-progress, the `controller_` should
    // have an associated icon animation.
    EnsureIconAnimationForKey(controller_key);

    // If `cumulative_progress_` is in an indeterminate state, an indeterminate
    // animation should be associated with the `controller_` (if one does not
    // already exist).
    if (cumulative_progress_.IsIndeterminate()) {
      EnsureRingAnimationOfTypeForKey(
          controller_key, ProgressRingAnimation::Type::kIndeterminate);
      return;
    }

    // If `cumulative_progress_` is not in an indeterminate state, the
    // `controller_` should not have an associated ring animation.
    registry_->SetProgressRingAnimationForKey(controller_key, nullptr);
  }

  // Invoked when the specified ring `animation` for the specified `key` has
  // been updated. This is used to clean up finished animations.
  void OnRingAnimationUpdatedForKey(AnimationKey key,
                                    ProgressRingAnimation* animation) {
    if (animation->IsAnimating())
      return;
    // Once `animation` has finished, it can be removed from the registry. Note
    // that this needs to be posted as it is illegal to delete `animation` from
    // its update callback sequence.
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(
            [](const base::WeakPtr<ProgressIndicatorAnimationDelegate>& self,
               AnimationKey key,
               MayBeDangling<ProgressRingAnimation> animation) {
              if (!self) {
                return;
              }
              auto* registry = self->registry_.get();
              if (registry->GetProgressRingAnimationForKey(key) == animation) {
                registry->SetProgressRingAnimationForKey(key, nullptr);
              }
            },
            weak_factory_.GetWeakPtr(), key, base::UnsafeDangling(animation)));
  }

  const raw_ptr<ProgressIndicatorAnimationRegistry, LeakedDanglingUntriaged>
      registry_;
  const raw_ptr<HoldingSpaceController, LeakedDanglingUntriaged> controller_;
  raw_ptr<HoldingSpaceModel, LeakedDanglingUntriaged> model_ = nullptr;

  // The cumulative progress for the attached `model_`, calculated and cached
  // with each call to `UpdateAnimations()`. This is used to determine when
  // cumulative progress changes from an incomplete to a completed state, at
  // which time a pulse animation is created and started.
  HoldingSpaceProgress cumulative_progress_;

  base::ScopedObservation<HoldingSpaceController,
                          HoldingSpaceControllerObserver>
      controller_observation_{this};

  base::ScopedObservation<HoldingSpaceModel, HoldingSpaceModelObserver>
      model_observation_{this};

  base::WeakPtrFactory<ProgressIndicatorAnimationDelegate> weak_factory_{this};
};

// HoldingSpaceAnimationRegistry -----------------------------------------------

HoldingSpaceAnimationRegistry::HoldingSpaceAnimationRegistry() {
  progress_indicator_animation_delegate_ =
      std::make_unique<ProgressIndicatorAnimationDelegate>(
          this, HoldingSpaceController::Get());

  shell_observation_.Observe(Shell::Get());
}

HoldingSpaceAnimationRegistry::~HoldingSpaceAnimationRegistry() = default;

// static
HoldingSpaceAnimationRegistry* HoldingSpaceAnimationRegistry::GetInstance() {
  auto& instance_owner = GetInstanceOwner();
  if (!instance_owner.get() && Shell::HasInstance())
    instance_owner.reset(new HoldingSpaceAnimationRegistry());
  return instance_owner.get();
}

void HoldingSpaceAnimationRegistry::OnShellDestroying() {
  auto& instance_owner = GetInstanceOwner();
  DCHECK_EQ(instance_owner.get(), this);
  instance_owner.reset();  // Deletes `this`.
}

}  // namespace ash