chromium/ash/ambient/model/ambient_topic_queue.h

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

#ifndef ASH_AMBIENT_MODEL_AMBIENT_TOPIC_QUEUE_H_
#define ASH_AMBIENT_MODEL_AMBIENT_TOPIC_QUEUE_H_

#include <memory>
#include <queue>
#include <utility>
#include <vector>

#include "ash/ash_export.h"
#include "ash/public/cpp/ambient/ambient_backend_controller.h"
#include "base/containers/flat_map.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "net/base/backoff_entry.h"

namespace ash {

class AmbientBackendController;

// Maintains a queue of topics fetched from the IMAX server. Note "topic" in
// this class refers to the early stage in the pipeline where the topic only has
// a primary (and optional related) photo url. AmbientTopicQueue does not deal
// with downloading/decoding actual photos.
//
// The AmbientTopicQueue automatically fills itself. The caller only has a Pop()
// API. It fills itself with new topics from IMAX when:
// * The caller has popped all existing topics from the queue (it's empty).
// * The |topic_fetch_interval| provided in the constructor has elapsed since
//   the last topic fetch. This is done so that a sufficient amount of topics
//   are available in the queue regardless of how often the caller calls Pop().
//   (The access token required to fetch topics is only valid for a fixed period
//   after entering ambient mode).
//
// The AmbientTopicQueue stops filling itself when the total number of topics it
// has fetched exceeds the |topic_fetch_limit| provided in the constructor.
// When this limit has been reached, the queue remains empty even after the
// caller Pop()s everything from it.
class ASH_EXPORT AmbientTopicQueue {
 public:
  class Delegate {
   public:
    virtual ~Delegate() = default;

    // Returns a non-empty set of sizes that specify the desired size of the
    // topic photos returned by IMAX. This is invoked before each topic fetch,
    // so implementations may change the set of sizes they specify if desired.
    //
    // The AmbientTopicQueue will make a best effort to maintain a uniform
    // distribution of the specified topic sizes in its queue. Note that the
    // |topic_fetch_limit| is always honored though and is a global limit for
    // all topics in the queue irrespective of size.
    virtual std::vector<gfx::Size> GetTopicSizes() = 0;
  };

  // Starts automatically filling the queue on construction. Note this class
  // intentionally does not have a method to clear the queue/reset its state.
  // For this, it's cheap to just destroy and re-create the queue.
  //
  // |topic_fetch_limit|: See class-level comments above.
  // |topic_fetch_size|: Number of topics requested from IMAX in each individual
  //                     topic fetch.
  // |topic_fetch_interval|: See class-level comments above.
  // |should_split_topics|: See |AmbientPhotoConfig.should_split_topics|.
  AmbientTopicQueue(int topic_fetch_limit,
                    int topic_fetch_size,
                    base::TimeDelta topic_fetch_interval,
                    bool should_split_topics,
                    Delegate* delegate,
                    AmbientBackendController* backend_controller);
  AmbientTopicQueue(const AmbientTopicQueue&) = delete;
  AmbientTopicQueue& operator=(const AmbientTopicQueue&) = delete;
  ~AmbientTopicQueue();

  // Invokes the |wait_cb| when the queue has topics in it (i.e. the queue is
  // not empty). If the queue already has topics in it, the |wait_cb| is invoked
  // immediately and synchronously.
  enum class WaitResult {
    // IsEmpty() is guaranteed to be false.
    kTopicsAvailable,
    // IsEmpty() will be true for the following:
    //
    // The most recent topic fetch failed, and the queue is currently backing
    // off, waiting for the IMAX server to recover. This result is returned
    // immediately so that the caller can implement any fallback logic it
    // has (ex: reading photos from an on-disc cache) rather than getting stuck
    // waiting for long periods of time.
    kTopicFetchBackingOff,
    // The |topic_fetch_limit| has been reached, so the queue will not be filled
    // with any more topics for the rest of its lifetime.
    kTopicFetchLimitReached,
  };
  using WaitCallback = base::OnceCallback<void(WaitResult)>;
  void WaitForTopicsAvailable(WaitCallback wait_cb);

  // Pops() the topic from the front of the queue. Fails with a fatal error if
  // IsEmpty().
  AmbientModeTopic Pop();

  // Whether or not there are any topics in the queue currently.
  bool IsEmpty() const;

 private:
  bool HasReachedTopicFetchLimit() const;

  void FetchTopics();
  void OnScreenUpdateInfoFetched(const base::RepeatingClosure& barrier_closure,
                                 const gfx::Size& requested_topic_size,
                                 const ash::ScreenUpdate& screen_update);
  void OnAllScreenUpdateInfoFetched();
  void ScheduleFetchTopics(bool backoff);
  void Push(AmbientModeTopic topic);
  void RunPendingWaitCallbacks(WaitResult wait_result);

  const int topic_fetch_limit_;
  const int topic_fetch_size_;
  const base::TimeDelta topic_fetch_interval_;
  const bool should_split_topics_;
  const raw_ptr<Delegate> delegate_;
  const raw_ptr<AmbientBackendController> backend_controller_;

  std::queue<AmbientModeTopic> available_topics_;
  int total_topics_fetched_ = 0;

  // Whether a topic fetch is current in progress (waiting for IMAX to respond).
  // For simplicity, there should only be one topic fetch active at any given
  // time.
  bool topic_fetch_in_progress_ = false;

  // For topic fetches scheduled in the future (either a regularly scheduled one
  // or one from backoff).
  base::OneShotTimer fetch_topic_timer_;

  // Backoff for fetch topics retries.
  net::BackoffEntry fetch_topic_retry_backoff_;

  // Accumulation of WaitForTopicsAvailable() calls made while a topic fetch
  // is currently in progress. These are flushed/run when the topic fetch
  // completes.
  std::vector<WaitCallback> pending_wait_cbs_;

  // Transient storage for the IMAX responses returned from each topic fetch.
  // There is one entry per topic size requested since there is also one network
  // request made per topic size.
  //
  // The key contains the requested topic size's width/height. Using a map
  // ensures a *deterministic* ordering of the topic sizes when processing this
  // map after each fetch, but the order itself is irrelevant.
  base::flat_map<std::pair<int, int>, std::vector<AmbientModeTopic>>
      pending_topic_batches_;

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

}  // namespace ash

#endif  // ASH_AMBIENT_MODEL_AMBIENT_TOPIC_QUEUE_H_