chromium/ash/ambient/ambient_photo_controller.cc

// Copyright 2019 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_photo_controller.h"

#include <algorithm>
#include <array>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "ash/ambient/ambient_backup_photo_downloader.h"
#include "ash/ambient/ambient_constants.h"
#include "ash/ambient/ambient_controller.h"
#include "ash/ambient/ambient_photo_cache.h"
#include "ash/ambient/model/ambient_backend_model.h"
#include "ash/public/cpp/ambient/ambient_backend_controller.h"
#include "ash/public/cpp/ambient/proto/photo_cache_entry.pb.h"
#include "ash/public/cpp/image_downloader.h"
#include "ash/public/cpp/image_util.h"
#include "ash/shell.h"
#include "base/barrier_closure.h"
#include "base/base64.h"
#include "base/base_paths.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/hash/sha1.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/system/sys_info.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "ui/gfx/image/image_skia.h"
#include "url/gurl.h"

namespace ash {

namespace {

// TODO(b/161357364): refactor utility functions and constants

constexpr net::BackoffEntry::Policy kResumeFetchImageBackoffPolicy = {
    kMaxConsecutiveReadPhotoFailures,  // Number of initial errors to ignore.
    500,                               // Initial delay in ms.
    2.0,            // Factor by which the waiting time will be multiplied.
    0.2,            // Fuzzing percentage.
    8 * 60 * 1000,  // Maximum delay in ms.
    -1,             // Never discard the entry.
    true,           // Use initial delay.
};

base::TaskTraits GetTaskTraits() {
  return {base::MayBlock(), base::TaskPriority::USER_BLOCKING,
          base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN};
}

const std::array<const char*, 2>& GetBackupPhotoUrls() {
  return Shell::Get()
      ->ambient_controller()
      ->ambient_backend_controller()
      ->GetBackupPhotoUrls();
}

}  // namespace

AmbientPhotoController::AmbientPhotoController(
    AmbientViewDelegate& view_delegate,
    AmbientPhotoConfig photo_config,
    std::unique_ptr<AmbientTopicQueue::Delegate> topic_queue_delegate)
    : topic_queue_delegate_(std::move(topic_queue_delegate)),
      ambient_backend_model_(std::move(photo_config)),
      resume_fetch_image_backoff_(&kResumeFetchImageBackoffPolicy),
      access_token_controller_(
          Shell::Get()->ambient_controller()->access_token_controller()),
      task_runner_(
          base::ThreadPool::CreateSequencedTaskRunner(GetTaskTraits())) {
  CHECK(topic_queue_delegate_);
  CHECK(access_token_controller_);
  scoped_view_delegate_observation_.Observe(&view_delegate);
  ScheduleFetchBackupImages();
}

AmbientPhotoController::~AmbientPhotoController() = default;

void AmbientPhotoController::Init() {
  state_ = State::kPreparingNextTopicSet;
  topic_index_ = 0;
  retries_to_read_from_cache_ = kMaxNumberOfCachedImages;
  backup_retries_to_read_from_cache_ = GetBackupPhotoUrls().size();
  num_topics_prepared_ = 0;
  is_actively_preparing_topic_ = false;
  ambient_topic_queue_ = std::make_unique<AmbientTopicQueue>(
      /*topic_fetch_limit=*/ambient_backend_model_.photo_config().IsEmpty()
          ? 0
          : kMaxNumberOfCachedImages,
      /*topic_fetch_size=*/kTopicsBatchSize, kTopicFetchInterval,
      ambient_backend_model_.photo_config().should_split_topics,
      topic_queue_delegate_.get(),
      Shell::Get()->ambient_controller()->ambient_backend_controller());
}

void AmbientPhotoController::StartScreenUpdate() {
  if (state_ != State::kInactive) {
    DVLOG(3) << "AmbientPhotoController is already active. Ignoring "
                "StartScreenUpdate().";
    return;
  }

  Init();
  if (backup_photo_refresh_timer_.IsRunning()) {
    // Would use |timer_.FireNow()| but this does not execute if screen is
    // locked. Manually call the expected callback instead.
    backup_photo_refresh_timer_.Stop();
    FetchBackupImages();
  }
  StartPreparingNextTopic();
}

void AmbientPhotoController::StopScreenUpdate() {
  state_ = State::kInactive;
  resume_fetch_image_backoff_.Reset();
  ambient_backend_model_.Clear();
  ambient_topic_queue_.reset();
  weak_factory_.InvalidateWeakPtrs();
}

bool AmbientPhotoController::IsScreenUpdateActive() const {
  return state_ != State::kInactive;
}

void AmbientPhotoController::OnMarkerHit(AmbientPhotoConfig::Marker marker) {
  if (!ambient_backend_model_.photo_config().refresh_topic_markers.contains(
          marker)) {
    DVLOG(3) << "UI event " << marker
             << " does not trigger a topic refresh. Ignoring...";
    return;
  }

  DVLOG(3) << "UI event " << marker << " triggering topic refresh";
  switch (state_) {
    case State::kInactive:
      LOG(DFATAL) << "Received unexpected UI marker " << marker
                  << " while inactive";
      break;
    case State::kPreparingNextTopicSet:
      // The controller is still in the middle of preparing a topic from the
      // previous set (i.e. waiting on a callback or timer to fire). Resetting
      // |num_topics_prepared_| to 0 is enough, and the topic currently being
      // prepared will count towards the next set.
      DVLOG(4) << "Did not finished preparing current topic set in time. "
                  "Starting new set...";
      num_topics_prepared_ = 0;
      break;
    case State::kWaitingForNextMarker:
      state_ = State::kPreparingNextTopicSet;
      num_topics_prepared_ = 0;
      StartPreparingNextTopic();
      break;
  }
}

void AmbientPhotoController::ScheduleFetchBackupImages() {
  DVLOG(3) << __func__;
  if (backup_photo_refresh_timer_.IsRunning())
    return;

  backup_photo_refresh_timer_.Start(
      FROM_HERE,
      std::max(kBackupPhotoRefreshDelay,
               resume_fetch_image_backoff_.GetTimeUntilRelease()),
      base::BindOnce(&AmbientPhotoController::FetchBackupImages,
                     weak_factory_.GetWeakPtr()));
}

void AmbientPhotoController::FetchBackupImages() {
  active_backup_image_downloads_.clear();
  const auto& backup_photo_urls = GetBackupPhotoUrls();
  backup_retries_to_read_from_cache_ = backup_photo_urls.size();
  const std::vector<gfx::Size> target_sizes =
      topic_queue_delegate_->GetTopicSizes();
  size_t target_size_idx = 0;
  // Evenly distribute target photo sizes for the current `AmbientTheme` amongst
  // the backup photos so that the ambient UI has as much variety in photo size
  // to work with as possible.
  for (size_t i = 0; i < backup_photo_urls.size(); i++, target_size_idx++) {
    active_backup_image_downloads_.push_back(
        std::make_unique<AmbientBackupPhotoDownloader>(
            *access_token_controller_, i,
            target_sizes[target_size_idx % target_sizes.size()],
            backup_photo_urls[i],
            base::BindOnce(&AmbientPhotoController::OnBackupImageFetched,
                           weak_factory_.GetWeakPtr())));
  }
}

void AmbientPhotoController::OnBackupImageFetched(bool success) {
  if (!success) {
    // TODO(b/169807068) Change to retry individual failed images.
    active_backup_image_downloads_.clear();
    resume_fetch_image_backoff_.InformOfRequest(/*succeeded=*/false);
    LOG(WARNING) << "Downloading backup image failed.";
    ScheduleFetchBackupImages();
    return;
  }
  resume_fetch_image_backoff_.InformOfRequest(/*succeeded=*/true);
}

void AmbientPhotoController::OnTopicsAvailableInQueue(
    AmbientTopicQueue::WaitResult wait_result) {
  if (state_ != State::kPreparingNextTopicSet)
    return;

  switch (wait_result) {
    case AmbientTopicQueue::WaitResult::kTopicsAvailable:
      ReadPhotoFromTopicQueue();
      break;
    case AmbientTopicQueue::WaitResult::kTopicFetchBackingOff:
    case AmbientTopicQueue::WaitResult::kTopicFetchLimitReached:
      // If there are no topics in the queue, will try to read from disk cache.
      TryReadPhotoFromCache();
      break;
  }
}

void AmbientPhotoController::ResetImageData() {
  cache_entry_.Clear();

  image_ = gfx::ImageSkia();
  related_image_ = gfx::ImageSkia();
}

void AmbientPhotoController::ReadPhotoFromTopicQueue() {
  ResetImageData();
  DVLOG(3) << "Downloading topic photos";
  AmbientModeTopic topic = ambient_topic_queue_->Pop();
  ::ambient::Photo* photo = cache_entry_.mutable_primary_photo();
  photo->set_details(topic.details);
  photo->set_is_portrait(topic.is_portrait);
  photo->set_type(topic.topic_type);

  const int num_callbacks = (topic.related_image_url.empty()) ? 1 : 2;
  auto on_done = base::BarrierClosure(
      num_callbacks,
      base::BindOnce(&AmbientPhotoController::OnAllPhotoRawDataDownloaded,
                     weak_factory_.GetWeakPtr()));

  ambient_photo_cache::DownloadPhoto(
      topic.url, *access_token_controller_,
      base::BindOnce(&AmbientPhotoController::OnPhotoRawDataDownloaded,
                     weak_factory_.GetWeakPtr(),
                     /*is_related_image=*/false, on_done));

  if (!topic.related_image_url.empty()) {
    ::ambient::Photo* related_photo = cache_entry_.mutable_related_photo();
    related_photo->set_details(topic.related_details);
    related_photo->set_is_portrait(topic.is_portrait);
    related_photo->set_type(topic.topic_type);

    ambient_photo_cache::DownloadPhoto(
        topic.related_image_url, *access_token_controller_,
        base::BindOnce(&AmbientPhotoController::OnPhotoRawDataDownloaded,
                       weak_factory_.GetWeakPtr(),
                       /*is_related_image=*/true, on_done));
  }
}

void AmbientPhotoController::TryReadPhotoFromCache() {
  ResetImageData();
  // Stop reading from cache after the max number of retries.
  if (retries_to_read_from_cache_ == 0) {
    if (backup_retries_to_read_from_cache_ == 0) {
      LOG(WARNING) << "Failed to read from cache";
      is_actively_preparing_topic_ = false;
      ambient_backend_model_.AddImageFailure();
      // Do not refresh image if image loading has failed repeatedly, or there
      // are no more topics to retry. Note |ambient_topic_queue_| may be null
      // if AddImageFailure() ultimately led to an AmbientBackendModelObserver
      // calling StopScreenUpdate().
      if (ambient_backend_model_.ImageLoadingFailed() ||
          !ambient_topic_queue_ || ambient_topic_queue_->IsEmpty()) {
        LOG(WARNING) << "Not attempting image refresh";
        return;
      }

      // Try to resume normal workflow with backoff.
      const base::TimeDelta delay =
          resume_fetch_image_backoff_.GetTimeUntilRelease();
      base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
          FROM_HERE,
          base::BindOnce(&AmbientPhotoController::StartPreparingNextTopic,
                         weak_factory_.GetWeakPtr()),
          delay);
      return;
    }

    --backup_retries_to_read_from_cache_;

    DVLOG(3) << "Read from backup cache index: "
             << backup_cache_index_for_display_;
    // Try to read a backup image.
    ambient_photo_cache::ReadPhotoCache(
        ambient_photo_cache::Store::kBackup,
        /*cache_index=*/backup_cache_index_for_display_,
        base::BindOnce(&AmbientPhotoController::OnPhotoCacheReadComplete,
                       weak_factory_.GetWeakPtr()));

    backup_cache_index_for_display_++;
    if (backup_cache_index_for_display_ == GetBackupPhotoUrls().size())
      backup_cache_index_for_display_ = 0;
    return;
  }

  --retries_to_read_from_cache_;
  int current_cache_index = cache_index_for_display_;

  ++cache_index_for_display_;
  if (cache_index_for_display_ == kMaxNumberOfCachedImages)
    cache_index_for_display_ = 0;

  DVLOG(3) << "Read from cache index: " << current_cache_index;
  ambient_photo_cache::ReadPhotoCache(
      ambient_photo_cache::Store::kPrimary, current_cache_index,
      base::BindOnce(&AmbientPhotoController::OnPhotoCacheReadComplete,
                     weak_factory_.GetWeakPtr()));
}

void AmbientPhotoController::OnPhotoCacheReadComplete(
    ::ambient::PhotoCacheEntry cache_entry) {
  cache_entry_ = std::move(cache_entry);
  OnAllPhotoRawDataAvailable(/*from_downloading=*/false);
}

void AmbientPhotoController::OnPhotoRawDataDownloaded(
    bool is_related_image,
    base::RepeatingClosure on_done,
    std::string&& data) {
  if (is_related_image)
    cache_entry_.mutable_related_photo()->set_image(std::move(data));
  else
    cache_entry_.mutable_primary_photo()->set_image(std::move(data));

  std::move(on_done).Run();
}

void AmbientPhotoController::OnAllPhotoRawDataDownloaded() {
  DVLOG(3) << __func__;
  OnAllPhotoRawDataAvailable(/*from_downloading=*/true);
}

void AmbientPhotoController::OnAllPhotoRawDataAvailable(bool from_downloading) {
  if (!cache_entry_.has_primary_photo() ||
      cache_entry_.primary_photo().image().empty()) {
    if (from_downloading) {
      LOG(ERROR) << "Failed to download image";
      resume_fetch_image_backoff_.InformOfRequest(/*succeeded=*/false);
    }
    // Try to read from cache when failure happens.
    TryReadPhotoFromCache();
    return;
  }

  const bool has_related = cache_entry_.has_related_photo() &&
                           !cache_entry_.related_photo().image().empty();
  const int num_callbacks = has_related ? 2 : 1;

  auto on_done = base::BarrierClosure(
      num_callbacks,
      base::BindOnce(
          &AmbientPhotoController::OnAllPhotoDecoded,
          weak_factory_.GetWeakPtr(), from_downloading,
          /*hash=*/base::SHA1HashString(cache_entry_.primary_photo().image())));

  DecodePhotoRawData(from_downloading,
                     /*is_related_image=*/false, on_done,
                     cache_entry_.primary_photo().image());

  if (has_related) {
    DecodePhotoRawData(from_downloading, /*is_related_image=*/true, on_done,
                       cache_entry_.related_photo().image());
  }
}

void AmbientPhotoController::SaveCurrentPhotoToCache() {
  // Note: WritePhotoCache could fail. The saved file name may not be
  // continuous.
  DVLOG(3) << "Save photo to cache index: " << cache_index_for_store_;
  auto current_cache_index = cache_index_for_store_;
  ++cache_index_for_store_;
  if (cache_index_for_store_ == kMaxNumberOfCachedImages) {
    cache_index_for_store_ = 0;
  }

  ambient_photo_cache::WritePhotoCache(
      ambient_photo_cache::Store::kPrimary,
      /*cache_index=*/current_cache_index, cache_entry_,
      base::BindOnce(
          [](int cache_index) {
            DVLOG(4) << "Done writing cache_index " << cache_index
                     << " to photo cache";
          },
          current_cache_index));
}
void AmbientPhotoController::DecodePhotoRawData(bool from_downloading,
                                                bool is_related_image,
                                                base::RepeatingClosure on_done,
                                                const std::string& data) {
  image_util::DecodeImageData(
      base::BindOnce(&AmbientPhotoController::OnPhotoDecoded,
                     weak_factory_.GetWeakPtr(), from_downloading,
                     is_related_image, std::move(on_done)),
      image_codec_, data);
}

void AmbientPhotoController::OnPhotoDecoded(bool from_downloading,
                                            bool is_related_image,
                                            base::RepeatingClosure on_done,
                                            const gfx::ImageSkia& image) {
  if (is_related_image)
    related_image_ = image;
  else
    image_ = image;

  std::move(on_done).Run();
}

void AmbientPhotoController::OnAllPhotoDecoded(bool from_downloading,
                                               const std::string& hash) {
  DVLOG(3) << __func__;
  DCHECK_EQ(state_, State::kPreparingNextTopicSet);
  DCHECK(is_actively_preparing_topic_);
  if (image_.isNull()) {
    LOG(WARNING) << "Image decoding failed";
    if (from_downloading)
      resume_fetch_image_backoff_.InformOfRequest(/*succeeded=*/false);

    // Try to read from cache when failure happens.
    TryReadPhotoFromCache();
    return;
  } else if (ambient_backend_model_.IsHashDuplicate(hash)) {
    LOG(WARNING) << "Skipping loading duplicate image.";
    TryReadPhotoFromCache();
    return;
  }

  if (from_downloading) {
    SaveCurrentPhotoToCache();
  }

  is_actively_preparing_topic_ = false;
  retries_to_read_from_cache_ = kMaxNumberOfCachedImages;
  backup_retries_to_read_from_cache_ = GetBackupPhotoUrls().size();

  if (from_downloading)
    resume_fetch_image_backoff_.InformOfRequest(/*succeeded=*/true);

  PhotoWithDetails detailed_photo;
  detailed_photo.photo = image_;
  detailed_photo.related_photo = related_image_;
  detailed_photo.details = cache_entry_.primary_photo().details();
  detailed_photo.related_details = cache_entry_.related_photo().details();
  detailed_photo.is_portrait = cache_entry_.primary_photo().is_portrait();
  detailed_photo.topic_type = cache_entry_.primary_photo().type();
  detailed_photo.hash = hash;

  ResetImageData();

  size_t target_num_topics_to_prepare =
      ambient_backend_model_.ImagesReady()
          ? ambient_backend_model_.photo_config().topic_set_size
          : ambient_backend_model_.photo_config().GetNumDecodedTopicsToBuffer();
  // AddNextImage() can call out to observers, who can synchronously interact
  // with the controller again within their observer notification methods. So
  // the internal |state_| and |num_topics_prepared_| should be updated and
  // captured in local variables before calling AddNextImage(). This ensures
  // that the behavior and state of the controller is consistent with the model.
  ++num_topics_prepared_;
  bool more_topics_required =
      num_topics_prepared_ < target_num_topics_to_prepare;
  if (!more_topics_required) {
    state_ = State::kWaitingForNextMarker;
  }

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

  if (more_topics_required) {
    StartPreparingNextTopic();
  }
}

void AmbientPhotoController::FetchTopicsForTesting() {
  StartPreparingNextTopic();
}

void AmbientPhotoController::FetchImageForTesting() {
  is_actively_preparing_topic_ = true;
  if (!ambient_topic_queue_->IsEmpty()) {
    ReadPhotoFromTopicQueue();
  } else {
    TryReadPhotoFromCache();
  }
}

void AmbientPhotoController::FetchBackupImagesForTesting() {
  FetchBackupImages();
}

void AmbientPhotoController::StartPreparingNextTopic() {
  DCHECK_EQ(state_, State::kPreparingNextTopicSet);
  if (ambient_backend_model_.photo_config().IsEmpty()) {
    DVLOG(1) << "No photos should be written to model";
    // This may not be necessary because a config like this probably doesn't
    // have any photo refresh markers anyways. However, it's more technically
    // correct to be in this state instead of |kPreparingNextTopicSet|.
    state_ = State::kWaitingForNextMarker;
    return;
  }
  DCHECK(!is_actively_preparing_topic_)
      << "Preparing multiple topics simultaneously is not currently supported";
  is_actively_preparing_topic_ = true;
  ambient_topic_queue_->WaitForTopicsAvailable(
      base::BindOnce(&AmbientPhotoController::OnTopicsAvailableInQueue,
                     weak_factory_.GetWeakPtr()));
}

std::ostream& operator<<(std::ostream& os,
                         AmbientPhotoController::State state) {
  switch (state) {
    case AmbientPhotoController::State::kInactive:
      return os << "INACTIVE";
    case AmbientPhotoController::State::kWaitingForNextMarker:
      return os << "WAITING_FOR_NEXT_MARKER";
    case AmbientPhotoController::State::kPreparingNextTopicSet:
      return os << "PREPARING_NEXT_TOPIC_SET";
  }
}

}  // namespace ash