chromium/ash/ambient/ui/ambient_animation_progress_tracker.cc

// Copyright 2022 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/ambient/ui/ambient_animation_progress_tracker.h"

#include <optional>
#include <utility>

#include "base/check.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"

namespace ash {

namespace {

void MoveAnimation(
    base::flat_set<raw_ptr<const lottie::Animation, CtnExperimental>>& from,
    base::flat_set<raw_ptr<const lottie::Animation, CtnExperimental>>& to,
    const lottie::Animation* animation) {
  if (to.contains(animation)) {
    CHECK(!from.contains(animation));
    return;
  }
  CHECK_EQ(from.erase(animation), 1u);
  to.insert(animation);
}

}  // namespace

AmbientAnimationProgressTracker::ImmutableParams::ImmutableParams() = default;

AmbientAnimationProgressTracker::ImmutableParams::ImmutableParams(
    const ImmutableParams& other) = default;

AmbientAnimationProgressTracker::ImmutableParams&
AmbientAnimationProgressTracker::ImmutableParams::operator=(
    const ImmutableParams& other) = default;

AmbientAnimationProgressTracker::ImmutableParams::~ImmutableParams() = default;

AmbientAnimationProgressTracker::AmbientAnimationProgressTracker() = default;

AmbientAnimationProgressTracker::~AmbientAnimationProgressTracker() = default;

void AmbientAnimationProgressTracker::RegisterAnimation(
    lottie::Animation* animation) {
  DCHECK(animation);
  if (animation_observations_.IsObservingSource(animation)) {
    return;
  }
  animation_observations_.AddObservation(animation);
  if (animation->GetPlaybackConfig()) {
    // The parameters verified here all concern "time" in the animation in some
    // form. They must match so that the "progress" returned by
    // GetGlobalProgress() has the same frame of reference across all
    // animations. Each animation's parameters are verified one time.
    VerifyAnimationImmutableParams(*animation);
    started_animations_.insert(animation);
  } else {
    DVLOG(4) << "Animation has not been Start()ed yet. Will be verified in "
                "AnimationWillStartPlaying() later.";
    inactive_animations_.insert(animation);
  }
}

bool AmbientAnimationProgressTracker::HasActiveAnimations() const {
  // Some of the started animations may not have painted a single frame yet. If
  // this is the case, GetCurrentProgress() will be null, so at least one of
  // them must return a non-null value for GetGlobalProgress() to return a valid
  // value.
  for (const lottie::Animation* animation : started_animations_) {
    if (animation->GetCurrentProgress())
      return true;
  }
  return false;
}

AmbientAnimationProgressTracker::Progress
AmbientAnimationProgressTracker::GetGlobalProgress() const {
  // Currently, the method for picking one "global" progress is trivial. It just
  // picks an arbitrary animation in the group because in practice, their
  // timestamps should not have diverged by an amount that is user-perceptible
  // (ex: less than 100 ms). If it's needed in the future, we can do something
  // more complex here like taking the median or mean progress of all
  // animations. But the complexity is currently not justified.
  for (const lottie::Animation* animation : started_animations_) {
    if (animation->GetCurrentProgress()) {
      DCHECK(animation->GetNumCompletedCycles());
      return {*animation->GetNumCompletedCycles(),
              *animation->GetCurrentProgress()};
    }
  }
  NOTREACHED() << "HasActiveAnimations() must be true before calling "
                  "GetGlobalProgress()";
}

AmbientAnimationProgressTracker::ImmutableParams
AmbientAnimationProgressTracker::GetImmutableParams() const {
  DCHECK(HasActiveAnimations());
  // The animation picked here is arbitrary since they all should have the same
  // immutable params.
  const lottie::Animation* animation = *started_animations_.begin();
  auto playback_config = animation->GetPlaybackConfig();
  ImmutableParams params;
  params.total_duration = animation->GetAnimationDuration();
  params.scheduled_cycles = playback_config->scheduled_cycles;
  params.style = playback_config->style;
  return params;
}

void AmbientAnimationProgressTracker::AnimationWillStartPlaying(
    const lottie::Animation* animation) {
  DCHECK(animation_observations_.IsObservingSource(
      const_cast<lottie::Animation*>(animation)));
  VerifyAnimationImmutableParams(*animation);
  MoveAnimation(/*from=*/inactive_animations_, /*to=*/started_animations_,
                animation);
}

void AmbientAnimationProgressTracker::AnimationStopped(
    const lottie::Animation* animation) {
  CHECK(animation_observations_.IsObservingSource(
      const_cast<lottie::Animation*>(animation)));
  MoveAnimation(/*from=*/started_animations_, /*to=*/inactive_animations_,
                animation);
}

void AmbientAnimationProgressTracker::AnimationIsDeleting(
    const lottie::Animation* animation) {
  DCHECK(animation_observations_.IsObservingSource(
      const_cast<lottie::Animation*>(animation)));
  animation_observations_.RemoveObservation(
      const_cast<lottie::Animation*>(animation));
  started_animations_.erase(animation);
  inactive_animations_.erase(animation);
}

void AmbientAnimationProgressTracker::VerifyAnimationImmutableParams(
    const lottie::Animation& animation) const {
  DCHECK(!started_animations_.contains(&animation));
  if (started_animations_.empty()) {
    DVLOG(4) << "Incoming animation is the first started in the session. No "
                "need to verify against other animations";
    return;
  }
  // The animation picked here is arbitrary since all existing animations should
  // have gone through this method, verifying that all of their immutable params
  // match.
  const lottie::Animation* existing_animation = *started_animations_.begin();
  DCHECK_EQ(animation.GetAnimationDuration(),
            existing_animation->GetAnimationDuration());
  auto incoming_playback_config = animation.GetPlaybackConfig();
  auto existing_playback_config = animation.GetPlaybackConfig();
  DCHECK(incoming_playback_config);
  DCHECK(existing_playback_config);
  DCHECK_EQ(incoming_playback_config->scheduled_cycles.size(),
            existing_playback_config->scheduled_cycles.size());
  for (size_t i = 0; i < incoming_playback_config->scheduled_cycles.size();
       ++i) {
    DCHECK_EQ(incoming_playback_config->scheduled_cycles[i].start_offset,
              existing_playback_config->scheduled_cycles[i].start_offset);
    DCHECK_EQ(incoming_playback_config->scheduled_cycles[i].end_offset,
              existing_playback_config->scheduled_cycles[i].end_offset);
  }
  DCHECK_EQ(incoming_playback_config->style, existing_playback_config->style);
}

}  // namespace ash