chromium/ash/ambient/ambient_managed_photo_controller.cc

// Copyright 2023 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/ambient_managed_photo_controller.h"

#include <utility>
#include <vector>

#include "ash/ambient/metrics/managed_screensaver_metrics.h"
#include "ash/ambient/model/ambient_backend_model.h"
#include "ash/public/cpp/image_util.h"
#include "base/check.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "ui/gfx/image/image_skia.h"

namespace ash {

namespace {
constexpr size_t kMinImagesRequired = 2u;

}  // namespace

AmbientManagedPhotoController::AmbientManagedPhotoController(
    AmbientViewDelegate& view_delegate,
    AmbientPhotoConfig photo_config)
    : ambient_backend_model_(std::move(photo_config)) {
  scoped_view_delegate_observation_.Observe(&view_delegate);
}

AmbientManagedPhotoController::~AmbientManagedPhotoController() = default;

void AmbientManagedPhotoController::StartScreenUpdate() {
  if (IsScreenUpdateActive()) {
    LOG(ERROR) << "AmbientManagedPhotoController is already active. Ignoring "
                  "StartScreenUpdate().";
    return;
  }

  is_active_ = true;
  image_attempt_no_ = 0;

  LoadImages();
}

void AmbientManagedPhotoController::UpdateImageFilePaths(
    const std::vector<base::FilePath>& images) {
  RecordManagedScreensaverImageCount(images.size());

  // Reset `error_state_` when a sufficient number of new images are received
  if (images.size() < kMinImagesRequired) {
    // TODO(b/269579804): Add Metrics
    SetErrorState(ErrorState::kInsufficientImages);
    return;
  }
  SetErrorState(ErrorState::kNone);

  images_file_paths_ = images;
  image_attempt_no_ = 0;

  if (IsScreenUpdateActive()) {
    // Invalidate the weak pointers to make sure that any in-flight decoding
    // operations become no-ops.
    weak_factory_.InvalidateWeakPtrs();
    current_image_index_ = 0;

    // Note: We do not clear the backend model here but rather just load
    // the next topic buffer size images from disk, this will automatically
    // fill the backend model with only the latest images.
    LoadImages();
  }
}

bool AmbientManagedPhotoController::HasScreenUpdateErrors() const {
  return error_state_ != ErrorState::kNone;
}

void AmbientManagedPhotoController::SetErrorState(ErrorState error_state) {
  if (error_state == error_state_) {
    return;
  }
  error_state_ = error_state;
  if (observer_) {
    observer_->OnErrorStateChanged();
  }
}

void AmbientManagedPhotoController::StopScreenUpdate() {
  is_active_ = false;
  images_file_paths_.clear();
  ambient_backend_model_.Clear();
  weak_factory_.InvalidateWeakPtrs();
  current_image_index_ = 0;
  image_attempt_no_ = 0;
}

bool AmbientManagedPhotoController::IsScreenUpdateActive() const {
  return is_active_;
}

void AmbientManagedPhotoController::OnMarkerHit(
    AmbientPhotoConfig::Marker marker) {
  if (!ambient_backend_model_.photo_config().refresh_topic_markers.contains(
          marker)) {
    DVLOG(3) << "UI event " << marker
             << " does not trigger a image refresh. Ignoring...";
    return;
  }
  if (error_state_ == ErrorState::kPhotoLoadFailure) {
    LOG(WARNING) << "Not loading the next image for the UI marker " << marker
                 << " as maximum photo loading attempts reached";
    return;
  }

  DVLOG(3) << "UI event " << marker << " triggering image load";
  if (!is_active_) {
    LOG(DFATAL) << "Received unexpected UI marker " << marker
                << " while inactive";
    return;
  }

  LoadImagesInternal(/*images_to_load=*/1, /*success=*/true);
}

void AmbientManagedPhotoController::LoadImages() {
  if (images_file_paths_.size() < kMinImagesRequired) {
    // TODO(b/269579804): Log metrics to detect if the images are not enough.
    LOG(WARNING)
        << "AmbientManagedPhotoController does not have enough images.";
    return;
  }

  // Initially load a total of backend model buffer size images in the backend
  // model. Note: This should not be lower than 2 because the photo view code
  // right now loads the current and the next image simultaneously, and in case
  // of a buffer size equal to 1 it never triggers the images ready callback.
  LoadImagesInternal(
      ambient_backend_model_.photo_config().GetNumDecodedTopicsToBuffer(),
      true);
}

void AmbientManagedPhotoController::LoadImagesInternal(size_t images_to_load,
                                                       bool success) {
  if (images_to_load == 0 || !success) {
    return;
  }
  // Use a partial function application to store the no of remaining images to
  // load.
  base::OnceCallback<void(bool)> done_callback = base::BindOnce(
      &AmbientManagedPhotoController::LoadImagesInternal,
      weak_factory_.GetWeakPtr(), /*images_to_load=*/images_to_load - 1);
  LoadNextImage(std::move(done_callback));
}

void AmbientManagedPhotoController::LoadNextImage(
    base::OnceCallback<void(bool success)> done_callback) {
  CHECK(images_file_paths_.size() >= kMinImagesRequired);
  image_attempt_no_++;
  current_image_index_ = (current_image_index_ + 1) % images_file_paths_.size();
  image_util::DecodeImageFile(
      base::BindOnce(&AmbientManagedPhotoController::OnPhotoDecoded,
                     weak_factory_.GetWeakPtr(), std::move(done_callback)),
      images_file_paths_.at(current_image_index_),
      data_decoder::mojom::ImageCodec::kDefault);
}

void AmbientManagedPhotoController::HandlePhotoDecodingFailure(
    base::OnceCallback<void(bool success)> done_callback) {
  if (image_attempt_no_ >= GetMaxImageAttempts()) {
    LOG(ERROR) << "Image decoding failed, no valid image was decoded";
    SetErrorState(ErrorState::kPhotoLoadFailure);
    std::move(done_callback).Run(false);
    return;
  }
  LOG(WARNING) << "Image decoding failed, attempting to load next image";
  LoadNextImage(std::move(done_callback));
}

void AmbientManagedPhotoController::OnPhotoDecoded(
    base::OnceCallback<void(bool success)> done_callback,
    const gfx::ImageSkia& image) {
  DVLOG(3) << __func__;
  if (image.isNull()) {
    HandlePhotoDecodingFailure(std::move(done_callback));
    return;
  }

  image_attempt_no_ = 0;
  PhotoWithDetails detailed_photo;
  detailed_photo.photo = image;
  // Note: There is no surefire way of determining the orientation of the image
  // so for now we assume and document that all admin provided images are
  // landscape.
  detailed_photo.is_portrait = false;

  ambient_backend_model_.AddNextImage(std::move(detailed_photo));

  // Notify the caller that photo loading was successful.
  std::move(done_callback).Run(true);
}

size_t AmbientManagedPhotoController::GetMaxImageAttempts() const {
  CHECK_GE(images_file_paths_.size(), kMinImagesRequired);
  return images_file_paths_.size() - 1;
}

void AmbientManagedPhotoController::SetObserver(Observer* observer) {
  CHECK(!observer_);
  observer_ = observer;
}

}  // namespace ash