chromium/ash/ambient/ambient_photo_controller.h

// 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.

#ifndef ASH_AMBIENT_AMBIENT_PHOTO_CONTROLLER_H_
#define ASH_AMBIENT_AMBIENT_PHOTO_CONTROLLER_H_

#include <memory>
#include <optional>
#include <ostream>
#include <string>
#include <utility>

#include "ash/ambient/ambient_constants.h"
#include "ash/ambient/model/ambient_backend_model.h"
#include "ash/ambient/model/ambient_photo_config.h"
#include "ash/ambient/model/ambient_topic_queue.h"
#include "ash/ambient/ui/ambient_view_delegate.h"
#include "ash/ash_export.h"
#include "ash/public/cpp/ambient/ambient_backend_controller.h"
#include "ash/public/cpp/ambient/proto/photo_cache_entry.pb.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/scoped_observation.h"
#include "base/task/sequenced_task_runner.h"
#include "base/timer/timer.h"
#include "net/base/backoff_entry.h"
#include "services/data_decoder/public/mojom/image_decoder.mojom-shared.h"
#include "services/network/public/cpp/simple_url_loader.h"

namespace gfx {
class ImageSkia;
}  // namespace gfx

namespace ash {

class AmbientAccessTokenController;
class AmbientBackupPhotoDownloader;

// Class to handle photos in ambient mode.
//
// Terminology:
//
// Topic - A primary and optional related photo specified by the IMAX server.
//
// Fetch Topics - Request new topics from the IMAX server. After they're
//                fetched, the controller just has urls for the primary/optional
//                photos in each returned topic.
//
// Download Topic - Download the encoded primary/related photos from their
//                  corresponding urls.
//
// Save Topic - Write the topic's encoded photos to disk for future re-use.
//              Helpful in future cases where ambient mode starts and there's no
//              internet.
//
// Load Topic - Read a previously saved topic's encoded photos from disk.
//
// Decode Topic - Decode the topic's photos and commit them to the
//                AmbientBackendModel.
//
// Prepare Topic - A term that aggregates all of the steps above:
// 1) Either a) fetch/download/save new topic or b) load existing topic
// 2) Decode topic and commit to the model.
//
// Topic Set - A group of topics for the UI to display in one cycle, capped by
// |AmbientPhotoConfig.topic_set_size|.
//
// The controller's state machine:
//
//        kInactive
//           |
//           |
//           v
// kPreparingNextTopicSet <-----
//           |                   |
//           |                   |
//           v                   |
// kWaitingForNextMarker -------
//
// kInactive:
// The controller is idle, and the model has no decoded topics in it. This is
// the initial state when the controller is constructed. Although not
// illustrated above, the controller can transition to this state from any of
// the other states via a call to StopScreenUpdate().
//
//
// kPreparingNextTopicSet (a.k.a. "refreshing" the model's topics):
// The very first time this state is reached, the UI has not started rendering
// yet, and the controller is preparing initial sets of topics. The
// AmbientPhotoConfig dictates how many sets to prepare initially. This state is
// initially triggered by a call to StartScreenUpdate(), and it ends when
// AmbientBackendModel::ImagesReady() is true.
//
// kWaitingForNextMarker:
// The UI is rendering the decoded topics currently in the model, and the
// controller is idle. It's waiting for the right marker(s) to be hit in the UI
// before it becomes active and starts preparing the next set of topics.
//
// kPreparingNextTopicSet (again):
// A target marker has been hit, and the controller immediately starts preparing
// the next set of topics. Unlike the first time this state was hit, there
// is only ever 1 topic set prepared, and the UI is rendering while the topics
// are being prepared. After the topic set is completely prepared, the
// controller goes back to WAITING_FOR_NEXT_MARKER. If another target marker is
// received while the controller is still preparing a topic set, the controller
// will simply reset its internal "counter" to 0 and start preparing a brand new
// set.
class ASH_EXPORT AmbientPhotoController : public AmbientViewDelegateObserver {
 public:
  AmbientPhotoController(
      AmbientViewDelegate& view_delegate,
      AmbientPhotoConfig photo_config,
      std::unique_ptr<AmbientTopicQueue::Delegate> topic_queue_delegate);

  AmbientPhotoController(const AmbientPhotoController&) = delete;
  AmbientPhotoController& operator=(const AmbientPhotoController&) = delete;

  ~AmbientPhotoController() override;

  // Start/stop updating the screen contents.
  void StartScreenUpdate();
  void StopScreenUpdate();
  bool IsScreenUpdateActive() const;

  AmbientBackendModel* ambient_backend_model() {
    return &ambient_backend_model_;
  }

  base::OneShotTimer& backup_photo_refresh_timer_for_testing() {
    return backup_photo_refresh_timer_;
  }

  // AmbientViewDelegateObserver:
  void OnMarkerHit(AmbientPhotoConfig::Marker marker) override;

 private:
  enum class State { kInactive, kWaitingForNextMarker, kPreparingNextTopicSet };

  friend class AmbientAshTestBase;
  friend class AmbientPhotoControllerTest;
  friend std::ostream& operator<<(std::ostream& os, State state);

  // Initialize variables.
  void Init();

  void ScheduleFetchBackupImages();

  // Download backup cache images.
  void FetchBackupImages();

  void OnBackupImageFetched(bool success);

  void OnTopicsAvailableInQueue(AmbientTopicQueue::WaitResult wait_result);

  // Clear temporary image data to prepare next photos.
  void ResetImageData();

  void ReadPhotoFromTopicQueue();

  void TryReadPhotoFromCache();

  void OnPhotoCacheReadComplete(::ambient::PhotoCacheEntry cache_entry);

  void OnPhotoRawDataDownloaded(bool is_related_image,
                                base::RepeatingClosure on_done,
                                std::string&& data);

  void OnAllPhotoRawDataDownloaded();

  void OnAllPhotoRawDataAvailable(bool from_downloading);

  void SaveCurrentPhotoToCache();

  void DecodePhotoRawData(bool from_downloading,
                          bool is_related_image,
                          base::RepeatingClosure on_done,
                          const std::string& data);

  void OnPhotoDecoded(bool from_downloading,
                      bool is_related_image,
                      base::RepeatingClosure on_done,
                      const gfx::ImageSkia& image);

  void OnAllPhotoDecoded(bool from_downloading,
                         const std::string& hash);

  void FetchTopicsForTesting();

  void FetchImageForTesting();

  void FetchBackupImagesForTesting();

  void set_image_codec_for_testing(
      data_decoder::mojom::ImageCodec image_codec) {
    image_codec_ = image_codec;
  }

  // Kicks off preparation of the next topic.
  void StartPreparingNextTopic();

  const std::unique_ptr<AmbientTopicQueue::Delegate> topic_queue_delegate_;
  std::unique_ptr<AmbientTopicQueue> ambient_topic_queue_;
  AmbientBackendModel ambient_backend_model_;

  // The timer to refresh backup cache photos.
  base::OneShotTimer backup_photo_refresh_timer_;

  State state_ = State::kInactive;

  // The index of a topic to download.
  size_t topic_index_ = 0;

  // Current index of cached image to read and display when failure happens.
  // The image file of this index may not exist or may not be valid. It will try
  // to read from the next cached file by increasing this index by 1.
  int cache_index_for_display_ = 0;

  // Current index of backup cached image to display when no other cached images
  // are available.
  size_t backup_cache_index_for_display_ = 0;

  // Current index of cached image to save for the latest downloaded photo.
  // The write command could fail. This index will increase 1 no matter writing
  // successfully or not. But theoretically we could not to change this index if
  // failures happen.
  int cache_index_for_store_ = 0;

  // Cached image may not exist or valid. This is the max times of attempts to
  // read cached images.
  int retries_to_read_from_cache_ = kMaxNumberOfCachedImages;

  int backup_retries_to_read_from_cache_ = 0;

  // Backoff to resume fetch images.
  net::BackoffEntry resume_fetch_image_backoff_;

  const raw_ptr<AmbientAccessTokenController> access_token_controller_;

  scoped_refptr<base::SequencedTaskRunner> task_runner_;

  // Temporary data store when fetching images and details.
  ::ambient::PhotoCacheEntry cache_entry_;
  gfx::ImageSkia image_;
  gfx::ImageSkia related_image_;

  // Tracks the number of topics that have been prepared since the controller
  // last transitioned to the |kPreparingNextTopicSet| state.
  size_t num_topics_prepared_ = 0;

  // This is purely for development purposes and does not contribute to the
  // user-facing business logic. It validates that only one topic is prepared at
  // a time. If multiple topics are prepared simultaneously, they may clobber
  // variables like |cache_entry_|, |image_|, etc and result in unpredictable
  // behavior.
  bool is_actively_preparing_topic_ = false;

  data_decoder::mojom::ImageCodec image_codec_ =
      data_decoder::mojom::ImageCodec::kDefault;

  base::ScopedObservation<AmbientViewDelegate, AmbientViewDelegateObserver>
      scoped_view_delegate_observation_{this};

  std::vector<std::unique_ptr<AmbientBackupPhotoDownloader>>
      active_backup_image_downloads_;

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

}  // namespace ash

#endif  // ASH_AMBIENT_AMBIENT_PHOTO_CONTROLLER_H_