chromium/ash/ambient/model/ambient_backend_model.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/model/ambient_backend_model.h"

#include <algorithm>
#include <random>
#include <utility>
#include <vector>

#include "ash/ambient/metrics/ambient_metrics.h"
#include "ash/ambient/model/ambient_backend_model_observer.h"
#include "ash/ambient/model/ambient_photo_config.h"
#include "ash/public/cpp/ambient/ambient_ui_model.h"
#include "ash/webui/personalization_app/mojom/personalization_app.mojom-shared.h"
#include "base/check.h"
#include "base/containers/flat_map.h"
#include "base/logging.h"
#include "base/time/time.h"

namespace ash {

namespace {

// Note this does not start until the minimum number of topics required in the
// AmbientPhotoConfig is reached.
constexpr base::TimeDelta kImagesReadyTimeout = base::Seconds(10);

}  // namespace

// PhotoWithDetails------------------------------------------------------------
PhotoWithDetails::PhotoWithDetails() = default;

PhotoWithDetails::PhotoWithDetails(const PhotoWithDetails&) = default;

PhotoWithDetails& PhotoWithDetails::operator=(const PhotoWithDetails&) =
    default;

PhotoWithDetails::PhotoWithDetails(PhotoWithDetails&&) = default;

PhotoWithDetails& PhotoWithDetails::operator=(PhotoWithDetails&&) = default;

PhotoWithDetails::~PhotoWithDetails() = default;

void PhotoWithDetails::Clear() {
  photo = gfx::ImageSkia();
  details = std::string();
  related_photo = gfx::ImageSkia();
  related_details = std::string();
  is_portrait = false;
}

bool PhotoWithDetails::IsNull() const {
  return photo.isNull();
}

// AmbientBackendModel---------------------------------------------------------
AmbientBackendModel::AmbientBackendModel(AmbientPhotoConfig photo_config) {
  SetPhotoConfig(std::move(photo_config));
}

AmbientBackendModel::~AmbientBackendModel() = default;

void AmbientBackendModel::AddObserver(AmbientBackendModelObserver* observer) {
  observers_.AddObserver(observer);
}

void AmbientBackendModel::RemoveObserver(
    AmbientBackendModelObserver* observer) {
  observers_.RemoveObserver(observer);
}

bool AmbientBackendModel::ImagesReady() const {
  DCHECK_LE(all_decoded_topics_.size(),
            photo_config_.GetNumDecodedTopicsToBuffer());
  return all_decoded_topics_.size() ==
             photo_config_.GetNumDecodedTopicsToBuffer() ||
         images_ready_timed_out_;
}

void AmbientBackendModel::OnImagesReadyTimeoutFired() {
  if (ImagesReady())
    return;

  DCHECK_GE(all_decoded_topics_.size(),
            photo_config_.min_total_topics_required);
  // TODO(esum): Add metrics for how often this case happens.
  LOG(WARNING) << "Timed out trying to prepare "
               << photo_config_.GetNumDecodedTopicsToBuffer()
               << " topics. Starting UI with " << all_decoded_topics_.size();
  images_ready_timed_out_ = true;
  NotifyImagesReady();
}

void AmbientBackendModel::AddNextImage(
    const PhotoWithDetails& photo_with_details) {
  DCHECK(!photo_with_details.IsNull());
  DCHECK(!photo_config_.IsEmpty())
      << "Photos should not be getting added to the model";

  ResetImageFailures();

  bool old_images_ready = ImagesReady();

  all_decoded_topics_.push_back(photo_with_details);
  while (all_decoded_topics_.size() >
         photo_config_.GetNumDecodedTopicsToBuffer()) {
    DCHECK(!all_decoded_topics_.empty());
    all_decoded_topics_.pop_front();
  }

  NotifyImageAdded();

  // Observers expect |OnImagesReady| after |OnImageAdded|.
  bool new_images_ready = ImagesReady();
  if (!old_images_ready && new_images_ready) {
    NotifyImagesReady();
    if (photo_with_details.topic_type == ::ambient::TopicType::kPersonal) {
      ambient::RecordAmbientModeTopicSource(
          personalization_app::mojom::TopicSource::kGooglePhotos);
    } else {
      ambient::RecordAmbientModeTopicSource(
          personalization_app::mojom::TopicSource::kArtGallery);
    }
  } else if (!new_images_ready &&
             all_decoded_topics_.size() >=
                 photo_config_.min_total_topics_required &&
             !images_ready_timeout_timer_.IsRunning()) {
    images_ready_timeout_timer_.Start(
        FROM_HERE, kImagesReadyTimeout, this,
        &AmbientBackendModel::OnImagesReadyTimeoutFired);
  }
}

bool AmbientBackendModel::IsHashDuplicate(const std::string& hash) const {
  // Make sure that a photo does not appear twice in a row.
  return all_decoded_topics_.empty() ? false
                                     : all_decoded_topics_.back().hash == hash;
}

void AmbientBackendModel::AddImageFailure() {
  failures_++;
  if (ImageLoadingFailed()) {
    DVLOG(3) << "image loading failed";
    for (auto& observer : observers_)
      observer.OnImagesFailed();
  }
}

void AmbientBackendModel::ResetImageFailures() {
  failures_ = 0;
}

bool AmbientBackendModel::ImageLoadingFailed() {
  return !ImagesReady() && failures_ >= kMaxConsecutiveReadPhotoFailures;
}

base::TimeDelta AmbientBackendModel::GetPhotoRefreshInterval() const {
  if (!ImagesReady())
    return base::TimeDelta();

  return AmbientUiModel::Get()->photo_refresh_interval();
}

void AmbientBackendModel::SetPhotoConfig(AmbientPhotoConfig photo_config) {
  photo_config_ = std::move(photo_config);
  DCHECK_LE(photo_config_.min_total_topics_required,
            photo_config_.GetNumDecodedTopicsToBuffer());
  Clear();
}

void AmbientBackendModel::Clear() {
  all_decoded_topics_.clear();
  images_ready_timeout_timer_.Stop();
  images_ready_timed_out_ = false;
}

void AmbientBackendModel::GetCurrentAndNextImages(
    PhotoWithDetails* current_image_out,
    PhotoWithDetails* next_image_out) const {
  auto fill_image_out = [&](size_t idx, PhotoWithDetails* image_out) {
    if (!image_out)
      return;

    image_out->Clear();
    if (idx < all_decoded_topics_.size()) {
      *image_out = all_decoded_topics_[idx];
    }
  };
  fill_image_out(/*idx=*/0, current_image_out);
  fill_image_out(/*idx=*/1, next_image_out);
}

void AmbientBackendModel::NotifyImageAdded() {
  for (auto& observer : observers_)
    observer.OnImageAdded();
}

void AmbientBackendModel::NotifyImagesReady() {
  for (auto& observer : observers_)
    observer.OnImagesReady();
}

}  // namespace ash