chromium/ash/system/focus_mode/focus_mode_tasks_model.cc

// Copyright 2024 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/system/focus_mode/focus_mode_tasks_model.h"

#include <absl/cleanup/cleanup.h>

#include <algorithm>

#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/observer_list.h"

namespace ash {

namespace {

// Returns a pointer to the task with a matching `task_id` from `tasks`. Returns
// nullptr if a matching task does not exist in `tasks`.
FocusModeTask* FindTaskById(const TaskId& task_id,
                            std::vector<FocusModeTask>& tasks) {
  auto iter = base::ranges::find(tasks, task_id, &FocusModeTask::task_id);
  return (iter == tasks.end()) ? nullptr : &(*iter);
}

void NotifyCompletedTask(
    base::ObserverList<FocusModeTasksModel::Observer>& observers,
    const FocusModeTask& task) {
  for (FocusModeTasksModel::Observer& observer : observers) {
    observer.OnTaskCompleted(task);
  }
}

void NotifySelectedTask(
    base::ObserverList<FocusModeTasksModel::Observer>& observers,
    const FocusModeTask* selected_task) {
  const std::optional<FocusModeTask> task =
      selected_task ? std::make_optional(*selected_task) : std::nullopt;
  for (FocusModeTasksModel::Observer& observer : observers) {
    observer.OnSelectedTaskChanged(task);
  }
}

void NotifyTaskListChanged(
    base::ObserverList<FocusModeTasksModel::Observer>& observers,
    const std::vector<FocusModeTask>& tasks) {
  for (FocusModeTasksModel::Observer& observer : observers) {
    observer.OnTasksUpdated(tasks);
  }
}

}  // namespace

FocusModeTasksModel::TaskUpdate::TaskUpdate() = default;
FocusModeTasksModel::TaskUpdate::TaskUpdate(const TaskUpdate&) = default;
FocusModeTasksModel::TaskUpdate::~TaskUpdate() = default;

// static
FocusModeTasksModel::TaskUpdate
FocusModeTasksModel::TaskUpdate::CompletedUpdate(const TaskId& task_id) {
  TaskUpdate update;
  update.task_id = task_id;
  update.completed = true;
  return update;
}

// static
FocusModeTasksModel::TaskUpdate FocusModeTasksModel::TaskUpdate::TitleUpdate(
    const TaskId& task_id,
    std::string_view title) {
  TaskUpdate update;
  update.task_id = task_id;
  update.title = std::string{title};
  return update;
}

// static
FocusModeTasksModel::TaskUpdate FocusModeTasksModel::TaskUpdate::NewTask(
    std::string_view title) {
  TaskUpdate update;
  update.title = std::string{title};
  return update;
}

FocusModeTasksModel::FocusModeTasksModel() = default;

FocusModeTasksModel::~FocusModeTasksModel() = default;

void FocusModeTasksModel::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void FocusModeTasksModel::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

void FocusModeTasksModel::SetDelegate(base::WeakPtr<Delegate> delegate) {
  delegate_ = std::move(delegate);
}

void FocusModeTasksModel::RequestUpdate() {
  if (!tasks_.empty()) {
    NotifyTaskListChanged(observers_, tasks_);
  }

  if (selected_task_) {
    NotifySelectedTask(observers_, selected_task_);
  }

  if (delegate_) {
    // If there is a currently selected task, we fetch the task to see if the
    // title was updated or if it has been completed.
    if (selected_task_) {
      delegate_->FetchTask(
          selected_task_->task_id,
          base::BindOnce(&FocusModeTasksModel::OnSelectedTaskFetched,
                         weak_ptr_factory_.GetWeakPtr()));
      return;
    }

    delegate_->FetchTasks();
  }
}

bool FocusModeTasksModel::SetSelectedTask(const TaskId& task_id) {
  CHECK(!task_id.empty());
  auto iter = base::ranges::find(tasks_, task_id, &FocusModeTask::task_id);

  if (iter == tasks_.end()) {
    // 'task_id' was not found in the task list. Task cannot be selected.
    return false;
  }

  // Clear the pref task since an existing task is selected.
  pref_task_id_.reset();

  if (selected_task_ && selected_task_->task_id == task_id) {
    // Task is the same as the currently selected task. Skip update.
    return true;
  }

  // Move selected task to the front of `tasks_` if it is not already there.
  if (iter != tasks_.begin()) {
    // Save `pending_task_` to be found later.
    const std::optional<TaskId> pending_task_id =
        pending_task_ ? std::make_optional(pending_task_->task_id)
                      : std::nullopt;

    pending_task_ = nullptr;
    selected_task_ = nullptr;
    // Move the selected task to the front.
    const auto desired = iter;
    iter++;
    std::rotate(tasks_.begin(), desired, iter);
    if (pending_task_id) {
      pending_task_ = FindTaskById(*pending_task_id, tasks_);
    }
  }
  selected_task_ = &tasks_[0];
  NotifySelectedTask(observers_, selected_task_);
  return true;
}

void FocusModeTasksModel::SetSelectedTask(const FocusModeTask& task) {
  CHECK(!task.task_id.empty());
  FocusModeTask* task_in_list = FindTaskById(task.task_id, tasks_);

  if (task_in_list) {
    *task_in_list = task;
  } else {
    InsertTaskIntoTaskList(FocusModeTask(task));
    NotifyTaskListChanged(observers_, tasks_);
  }

  CHECK(SetSelectedTask(task.task_id));
}

void FocusModeTasksModel::ClearSelectedTask() {
  pref_task_id_.reset();
  if (selected_task_) {
    selected_task_ = nullptr;
    NotifySelectedTask(observers_, nullptr);
  }
}

void FocusModeTasksModel::Reset() {
  NotifySelectedTask(observers_, nullptr);
  NotifyTaskListChanged(observers_, {});
  selected_task_ = nullptr;
  pending_task_ = nullptr;
  pref_task_id_.reset();
  tasks_.clear();
}

void FocusModeTasksModel::SetSelectedTaskFromPrefs(const TaskId& task_id) {
  if (selected_task_) {
    // Never override a selected task with pref data.
    return;
  }

  FocusModeTask* matching_task = FindTaskById(task_id, tasks_);
  if (matching_task) {
    selected_task_ = matching_task;
    NotifySelectedTask(observers_, selected_task_);
    return;
  }

  // Preference task is not in the cache. Try to fetch it.
  pref_task_id_ = task_id;
  FocusModeTasksModel::Delegate::FetchTaskCallback callback = base::BindOnce(
      &FocusModeTasksModel::OnPrefTaskFetched, weak_ptr_factory_.GetWeakPtr());
  if (delegate_) {
    delegate_->FetchTask(*pref_task_id_, std::move(callback));
  }
}

void FocusModeTasksModel::SetTaskList(std::vector<FocusModeTask>&& tasks) {
  TaskId desired_task_id;
  if (selected_task_) {
    desired_task_id = selected_task_->task_id;
  } else if (pref_task_id_) {
    desired_task_id = *pref_task_id_;
  }

  bool desired_task_in_list = false;
  if (!desired_task_id.empty()) {
    FocusModeTask* new_selected_task = FindTaskById(desired_task_id, tasks);
    if (new_selected_task) {
      desired_task_in_list = true;
    } else if (selected_task_) {
      // There is a selected task but its not in `tasks`. Add it ourselves.
      tasks.insert(tasks.begin(), *selected_task_);
      desired_task_in_list = true;
    }
  }

  // Clear pointers to tasks in the old list since they will become invalid.
  pending_task_ = nullptr;
  selected_task_ = nullptr;
  tasks_ = tasks;

  if (desired_task_in_list) {
    // Find the task again since the address changed after `tasks_` was
    // replaced.
    selected_task_ = FindTaskById(desired_task_id, tasks_);
    pref_task_id_.reset();
  }

  NotifyTaskListChanged(observers_, tasks_);

  if (desired_task_id.empty()) {
    // Only notify if there was an id we were looking for.
    return;
  }

  if (pref_task_id_) {
    // Preference tasks might not be in the list. Wait for the fetch request
    // to return before notification.
    return;
  }

  NotifySelectedTask(observers_, selected_task_);
}

void FocusModeTasksModel::UpdateTask(const TaskUpdate& task_update) {
  FocusModeTask* task = nullptr;
  if (task_update.task_id) {
    // Find in list.
    task = FindTaskById(*task_update.task_id, tasks_);
  } else {
    // New task
    FocusModeTask new_task;
    new_task.task_id.pending = true;
    selected_task_ = nullptr;
    pending_task_ = nullptr;
    task = &(*tasks_.insert(tasks_.begin(), std::move(new_task)));
    // New tasks must always become the selected task.
    pending_task_ = task;
    selected_task_ = pending_task_;
  }

  if (task == nullptr) {
    return;
  }

  if (task_update.title) {
    task->title = *task_update.title;
  }

  if (task_update.completed.has_value()) {
    CHECK(task_update.task_id);
    const TaskId& id = *task_update.task_id;
    auto iter = base::ranges::find(tasks_, id, &FocusModeTask::task_id);
    CHECK(iter != tasks_.end());

    NotifyCompletedTask(observers_, *iter);
    if (selected_task_ == task) {
      if (pending_task_ == selected_task_) {
        pending_task_ = nullptr;
      }
      selected_task_ = nullptr;
    }
    tasks_.erase(iter);
  }

  if (delegate_) {
    if (!task_update.task_id) {
      delegate_->AddTask(task_update,
                         base::BindOnce(&FocusModeTasksModel::OnTaskAdded,
                                        weak_ptr_factory_.GetWeakPtr()));
    } else {
      if (!task_update.task_id->pending) {
        // Pending tasks don't exist on the server so this is invalid.
        delegate_->UpdateTask(task_update);
      }
    }
  }

  NotifyTaskListChanged(observers_, tasks_);
  NotifySelectedTask(observers_, selected_task_);
}

const std::vector<FocusModeTask>& FocusModeTasksModel::tasks() const {
  return tasks_;
}

const FocusModeTask* FocusModeTasksModel::selected_task() const {
  return selected_task_;
}

const TaskId& FocusModeTasksModel::PrefTaskIdForTesting() const {
  return *pref_task_id_;
}

void FocusModeTasksModel::OnTaskAdded(
    const std::optional<FocusModeTask>& fetched_task) {
  if (!pending_task_) {
    LOG(WARNING) << "Update for a task that is no longer pending";
    return;
  }

  if (!pending_task_->task_id.pending) {
    LOG(WARNING) << "Pending task already has an id";
    return;
  }

  if (!fetched_task) {
    LOG(WARNING) << "Adding task failed";
    return;
  }

  // Update the pending task (in place) with the new data.
  *pending_task_ = *fetched_task;
  if (pending_task_ == selected_task_) {
    NotifySelectedTask(observers_, selected_task_);
  }

  // Clear the pending task.
  pending_task_ = nullptr;
}

void FocusModeTasksModel::OnPrefTaskFetched(
    const std::optional<FocusModeTask>& fetched_task) {
  if (!fetched_task) {
    LOG(WARNING) << "Fetching Pref task failed. Try again later";
    return;
  }
  if (selected_task_ || !pref_task_id_) {
    // If a task was selected while we were waiting, discard the response and
    // the pref task.
    pref_task_id_.reset();
    return;
  }

  const TaskId& task_id = fetched_task->task_id;
  if (task_id != *pref_task_id_) {
    // Fetched task does not match the task we are currently looking for.
    return;
  }

  // Controls if the list update is emitted.
  bool list_updated = true;

  auto iter = base::ranges::find(tasks_, task_id, &FocusModeTask::task_id);
  if (iter != tasks_.end()) {
    if (fetched_task->completed) {
      tasks_.erase(iter);
    } else {
      // Suppress updates if the task is in the list.
      list_updated = false;
      selected_task_ = &(*iter);
    }
  } else {
    if (!fetched_task->completed) {
      selected_task_ = InsertTaskIntoTaskList(FocusModeTask(*fetched_task));
    }
  }

  pref_task_id_.reset();

  if (list_updated) {
    NotifyTaskListChanged(observers_, tasks_);
  }
  NotifySelectedTask(observers_, selected_task_);
}

void FocusModeTasksModel::OnSelectedTaskFetched(
    const std::optional<FocusModeTask>& fetched_task) {
  CHECK(delegate_);

  if (!fetched_task) {
    LOG(WARNING) << "Fetching Selected task failed. Try again later";
    return;
  }

  // Ensure that `FetchTasks` is called afterwards. This will trigger a
  // `NotifyTaskListChanged`.
  absl::Cleanup fetch_tasks = [this] { delegate_->FetchTasks(); };

  const TaskId& task_id = fetched_task->task_id;
  FocusModeTask* task = FindTaskById(task_id, tasks_);
  if (task == nullptr) {
    // 'task_id' was not found in the task list. Nothing to update.
    return;
  }

  // Update the title if it has changed.
  bool has_task_title_changed = task->title != fetched_task->title;
  if (has_task_title_changed) {
    task->title = fetched_task->title;
  }

  if (fetched_task->completed) {
    UpdateTask(TaskUpdate::CompletedUpdate(task_id));
    return;
  }

  if (has_task_title_changed && selected_task_ &&
      selected_task_->task_id == task_id) {
    NotifySelectedTask(observers_, selected_task_);
  }
}

FocusModeTask* FocusModeTasksModel::InsertTaskIntoTaskList(
    FocusModeTask&& task) {
  std::optional<TaskId> selected_task_id =
      selected_task_ ? std::make_optional(selected_task_->task_id)
                     : std::nullopt;
  std::optional<TaskId> pending_task_id =
      pending_task_ ? std::make_optional(pending_task_->task_id) : std::nullopt;

  pending_task_ = nullptr;
  selected_task_ = nullptr;
  auto inserted = tasks_.insert(tasks_.begin(), task);

  if (selected_task_id) {
    selected_task_ = FindTaskById(*selected_task_id, tasks_);
  }
  if (pending_task_id) {
    // `pending_task_` and `selected_task_` are frequently the same. Skip the
    // search if possible.
    if (pending_task_id == selected_task_id) {
      pending_task_ = selected_task_;
    } else {
      pending_task_ = FindTaskById(*pending_task_id, tasks_);
    }
  }

  return &(*inserted);
}

}  // namespace ash