// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_UI_ASH_GLANCEABLES_GLANCEABLES_CLASSROOM_CLIENT_IMPL_H_
#define CHROME_BROWSER_UI_ASH_GLANCEABLES_GLANCEABLES_CLASSROOM_CLIENT_IMPL_H_
#include <list>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "ash/glanceables/classroom/glanceables_classroom_client.h"
#include "ash/glanceables/classroom/glanceables_classroom_types.h"
#include "base/containers/flat_map.h"
#include "base/functional/callback_forward.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/types/expected.h"
#include "chrome/browser/ui/ash/glanceables/glanceables_classroom_course_work_item.h"
#include "google_apis/common/api_error_codes.h"
#include "google_apis/common/request_sender.h"
class Profile;
namespace base {
class Clock;
class Time;
} // namespace base
namespace google_apis::classroom {
class Courses;
class CourseWork;
class StudentSubmissions;
} // namespace google_apis::classroom
namespace net {
struct NetworkTrafficAnnotationTag;
} // namespace net
namespace ash {
// Provides implementation for `GlanceablesClassroomClient`. Responsible for
// communication with Google Classroom API.
class GlanceablesClassroomClientImpl : public GlanceablesClassroomClient {
public:
// Provides an instance of `google_apis::RequestSender` for the client.
using CreateRequestSenderCallback =
base::RepeatingCallback<std::unique_ptr<google_apis::RequestSender>(
const std::vector<std::string>& scopes,
const net::NetworkTrafficAnnotationTag& traffic_annotation_tag)>;
using SortComparator = base::RepeatingCallback<bool(
const GlanceablesClassroomCourseWorkItem* lhs,
const GlanceablesClassroomCourseWorkItem* rhs)>;
GlanceablesClassroomClientImpl(
Profile* profile,
base::Clock* clock,
const CreateRequestSenderCallback& create_request_sender_callback);
GlanceablesClassroomClientImpl(const GlanceablesClassroomClientImpl&) =
delete;
GlanceablesClassroomClientImpl& operator=(
const GlanceablesClassroomClientImpl&) = delete;
~GlanceablesClassroomClientImpl() override;
// GlanceablesClassroomClient:
bool IsDisabledByAdmin() const override;
void IsStudentRoleActive(IsRoleEnabledCallback callback) override;
void GetCompletedStudentAssignments(GetAssignmentsCallback callback) override;
void GetStudentAssignmentsWithApproachingDueDate(
GetAssignmentsCallback callback) override;
void GetStudentAssignmentsWithMissedDueDate(
GetAssignmentsCallback callback) override;
void GetStudentAssignmentsWithoutDueDate(
GetAssignmentsCallback callback) override;
void OnGlanceablesBubbleClosed() override;
private:
FRIEND_TEST_ALL_PREFIXES(GlanceablesClassroomClientImplTest, FetchCourses);
FRIEND_TEST_ALL_PREFIXES(GlanceablesClassroomClientImplTest,
FetchCoursesOnHttpError);
FRIEND_TEST_ALL_PREFIXES(GlanceablesClassroomClientImplTest,
FetchCoursesMultiplePages);
FRIEND_TEST_ALL_PREFIXES(GlanceablesClassroomClientImplTest, FetchCourseWork);
FRIEND_TEST_ALL_PREFIXES(GlanceablesClassroomClientImplTest,
FetchCourseWorkAndSubmissions);
FRIEND_TEST_ALL_PREFIXES(GlanceablesClassroomClientImplTest,
FetchCourseWorkOnHttpError);
FRIEND_TEST_ALL_PREFIXES(GlanceablesClassroomClientImplTest,
FetchCourseWorkMultiplePages);
FRIEND_TEST_ALL_PREFIXES(GlanceablesClassroomClientImplTest,
FetchCourseWorkAndSubmissionsMultiplePages);
FRIEND_TEST_ALL_PREFIXES(GlanceablesClassroomClientImplTest,
FetchStudentSubmissions);
FRIEND_TEST_ALL_PREFIXES(GlanceablesClassroomClientImplTest,
FetchStudentSubmissionsOnHttpError);
FRIEND_TEST_ALL_PREFIXES(GlanceablesClassroomClientImplTest,
FetchStudentSubmissionsMultiplePages);
FRIEND_TEST_ALL_PREFIXES(GlanceablesClassroomClientImplTest,
FetchCourseWorkAfterStudentSubmissions);
// Done callback for fetching all courses for student or teacher roles.
using CourseList = std::vector<std::unique_ptr<GlanceablesClassroomCourse>>;
using FetchCoursesCallback =
base::OnceCallback<void(bool success, const CourseList& courses)>;
using CourseWorkInfo =
base::flat_map<std::string, GlanceablesClassroomCourseWorkItem>;
using CourseWorkPerCourse = base::flat_map<std::string, CourseWorkInfo>;
enum class FetchStatus {
// The data needs to be fetched - either because it was never fetched, or
// glanceables bubble was closed since the data was last fetched.
kNotFetched,
// The data fetch is in progress.
kFetching,
// The data fetch is in progress, but the glanceables bubble was closed
// before the fetch finished.
kFetchingInvalidated,
// The data has been fetched.
kFetched
};
// Flavours of course work data handled by the client.
enum class CourseWorkType { kStudent, kTeacher };
// Tracks a course list state - the latest fetched list, the fetch status, and
// the list of callbacks waiting for the list to be fetched.
class CourseListState {
public:
explicit CourseListState(base::Clock* clock);
CourseListState(const CourseListState&) = delete;
CourseListState& operator=(const CourseListState&) = delete;
~CourseListState();
// If the course list is fetched, it runs the `callback` immediately.
// Otherwise, it enqueues the callback to be run when the list gets fetched.
// It updates the fetch status to indicate that the list is to be fetched.
// Returns whether the client should initiate course list request.
bool RunOrEnqueueCallbackAndUpdateFetchStatus(
FetchCoursesCallback callback);
// Called by the `GlanceablesClassroomClientImpl` to update the course list
// state when a course list fetch request completes.
// It updates the cached course list, and updates the fetch state, and runs
// any pending course list callbacks.
// `fetched_courses` - the list of fetched courses, nullptr if the course
// list fetch request failed.
void FinalizeFetch(std::unique_ptr<CourseList> fetched_courses);
const CourseList& courses() const { return courses_; }
private:
// Runs pending callbacks in `callbacks_` and passes them the latest cached
// course list.
void RunCallbacks(bool success);
CourseList courses_;
FetchStatus fetch_status_ = FetchStatus::kNotFetched;
const raw_ptr<base::Clock> clock_;
base::Time last_successful_fetch_time_;
std::vector<FetchCoursesCallback> callbacks_;
};
// Wrapper around course work fetch callback that tracks the number of pending
// course work page requests.
// While individual pages need to be fetched serially, course work fetch may
// require fetching student submissions for course work in each of the course
// work pages. In that case, a page request is deemed complete when all
// required student submissions are fetched. Fetching student submissions for
// a course work page does not block fetch of the next course work page, which
// means that handling of different course work pages may overlap.
class CourseWorkRequest {
public:
explicit CourseWorkRequest(base::OnceClosure callback);
CourseWorkRequest(const CourseWorkRequest&) = delete;
CourseWorkRequest& operator=(const CourseWorkRequest&) = delete;
~CourseWorkRequest();
// Increases the count of pending course work page requests - should be
// called when a fetch for a course work page is initiated.
void IncrementPendingPageCount();
// Decrease the count of pending course work page requests - should be
// called when a fetch for a course work page, including student submissions
// data (when required) completes.
void DecrementPendingPageCount();
// If no more page tokens are pending, runs the `callback_`.
// Returns whether the callback was run. If the callback is run, the object
// can be discarded. and `RespondIfComplete()` should not be called any
// longer.
bool RespondIfComplete();
private:
base::OnceClosure callback_;
int pending_page_requests_ = 0;
};
// Updates the `*fetch_status` in response to the glanceables bubble closing -
// it updates the fetch status to indicate that the data can be refetched when
// requested again.
static void InvalidateFetchStatus(FetchStatus* fetch_status);
// Whether student submissions should be fetched per course work item, or per
// course.
bool ShouldFetchSubmissionsPerCourseWork(
CourseWorkType course_work_type) const;
// Gets a reference to course work data for the provided course work type.
CourseWorkPerCourse& GetCourseWork(CourseWorkType type);
// Called when an API call to get part of course work data for the course work
// type fails.
void SetCourseWorkFetchHadFailure(CourseWorkType type);
// Fetches all courses for student and teacher roles and invokes `callback`
// when done.
void FetchStudentCourses(FetchCoursesCallback callback);
// Fetches all course work items for the specified `course_id` and invokes
// `callback` when done. The course work information is saved in the course
// work map for `course_work_type` (either `student_course_work_` or
// `teacher_course_work_`).
void FetchCourseWork(const std::string& course_id,
CourseWorkType course_work_type,
base::OnceClosure callback);
// Fetches all student submissions for the specified `course_id` and
// `course_work_id` and invokes `callback` when done.
// To requests student submissions for all course work item in the course,
// pass in `course_work_id` value "-".
// The fetched student submissions get added to the course work map for
// `course_work_type` (either `student_course_work_` or
// `teacher_course_work_`).
void FetchStudentSubmissions(const std::string& course_id,
const std::string& course_work_id,
CourseWorkType course_work_type,
base::OnceClosure callback);
// Delays executing `callback` until all student data are fetched.
void InvokeOnceStudentDataFetched(base::OnceClosure callback);
// Fetches one page of courses.
// `page_token` - token specifying the result page to return, comes
// from the previous fetch request. Use an empty string
// to fetch the first page.
// `fetched_courses` - the container to which course items returned during
// course list fetch are saved. This container will be
// passed to `callback` once all items have been
// fetched.
void FetchCoursesPage(const std::string& page_token,
std::unique_ptr<CourseList> fetched_courses);
// Callback for `FetchCoursesPage()`. If `next_page_token()` in the `result`
// is not empty - calls another `FetchCoursesPage()`, otherwise runs done
// `callback`.
void OnCoursesPageFetched(
std::unique_ptr<CourseList> fetched_courses,
const base::Time& request_start_time,
base::expected<std::unique_ptr<google_apis::classroom::Courses>,
google_apis::ApiErrorCode> result);
// Callback for `FetchStudentCourses()`. Triggers fetching course work and
// student submissions for fetched courses and invokes
// `on_course_work_and_student_submissions_fetched` when done.
// `course_work_type` indicates the flavour of course work information that's
// being fetched, and is used to determine the course work map where the
// course work and student submissions whose fetch gets requested should be
// saved.
void OnCoursesFetched(
CourseWorkType course_work_type,
base::OnceClosure on_course_work_and_student_submissions_fetched,
bool success,
const CourseList& target_course_list);
// Fetches one page of course work items.
// `request_id` - the ID for the course work request that's being
// handled. It can be used to get the associated
// `CourseWorkRequest` from `course_work_requests_`.
// `course_id` - identifier of the course.
// `page_token` - token specifying the result page to return, comes from
// the previous fetch request. Use an empty string to
// fetch the first page.
// `page_number` - 1-based page number of this fetch request. Used for
// UMA to track the total number of pages needed to
// fetch.
// `course_work_type` - The flavour of course work information being fetched.
// Determines the course work map where course work
// information gets saved, and whether student
// submissions need to be fetched per course work item.
void FetchCourseWorkPage(int request_id,
const std::string& course_id,
const std::string& page_token,
int page_number,
CourseWorkType course_work_type);
// Callback for `FetchCourseWorkPage()`. If `next_page_token()` in the
// `result` is not empty - calls another `FetchCourseWorkPage()`, otherwise
// runs done `callback`.
void OnCourseWorkPageFetched(
int request_id,
const std::string& course_id,
CourseWorkType course_work_type,
const base::Time& request_start_time,
int page_number,
base::expected<std::unique_ptr<google_apis::classroom::CourseWork>,
google_apis::ApiErrorCode> result);
// Fetches one page of student submissions.
// `course_id` - identifier of the course.
// `course_work_id` - identifier of the course work item. May be "-" to
// request student submissions for all course work in the
// course.
// `page_token` - token specifying the result page to return, comes from
// the previous fetch request. Use an empty string to
// fetch the first page.
// `page_number` - 1-based page number of this fetch request. Used for
// UMA to track the total number of pages needed to
// fetch.
// `course_work_type` - The flavour of course work information being fetched.
// Determines the course work map where student
// submissions information gets saved.
// `callback` - a callback that runs when all student submissions in a
// course have been fetched. This may require multiple
// fetch requests, in this case `callback` gets called
// when the final request completes.
void FetchStudentSubmissionsPage(const std::string& course_id,
const std::string& course_work_id,
const std::string& page_token,
int page_number,
CourseWorkType course_work_type,
base::OnceClosure callback);
// Callback for `FetchStudentSubmissionsPage()`. If `next_page_token()` in the
// `result` is not empty - calls another `FetchStudentSubmissionsPage()`,
// otherwise runs done `callback`.
void OnStudentSubmissionsPageFetched(
const std::string& course_id,
const std::string& course_work_id,
CourseWorkType course_work_type,
const base::Time& request_start_time,
int page_number,
base::OnceClosure callback,
base::expected<
std::unique_ptr<google_apis::classroom::StudentSubmissions>,
google_apis::ApiErrorCode> result);
// Callback for requests to fetch student submissions for all course work
// items within a course work list page. The student submissions fetch is a
// subtask of a course work request, which is identified by `request_id`.
// When processing a page in course work list response, student submissions
// may get requested for each course work item - this callback is called
// when all requested student submission lists have been fetched.
void OnCourseWorkSubmissionsFetched(int request_id,
const std::string& course_id);
// Invokes all pending callbacks from `callbacks_waiting_for_student_data_`
// once all student data are fetched (courses + course work + student
// submissions).
void OnStudentDataFetched(const base::Time& sequence_start_time);
// Selects student assignments that satisfy both filtering predicates below.
// `due_predicate` - returns `true` if passed due date/time
// satisfies filtering requirements.
// `submission_state_predicate` - returns `true` if passed submission state
// satisfies filtering requirements.
// `sort_comparator` - the function used when comparing two
// assignments for sorting.
// `callback` - invoked with filtered results.
void GetFilteredStudentAssignments(
base::RepeatingCallback<bool(const std::optional<base::Time>&)>
due_predicate,
base::RepeatingCallback<bool(GlanceablesClassroomStudentSubmissionState)>
submission_state_predicate,
SortComparator sort_comparator,
GetAssignmentsCallback callback);
// Removes all invalid course work items from `course_work` for courses in
// the course list.
void PruneInvalidCourseWork(const CourseList& courses,
CourseWorkPerCourse& course_work);
// Returns lazily initialized `request_sender_`.
google_apis::RequestSender* GetRequestSender();
// The profile for which this instance was created.
const raw_ptr<Profile> profile_;
// Clock to be used to retrieve current time - expected to be default clock in
// production.
const raw_ptr<base::Clock> clock_;
// Callback passed from `GlanceablesKeyedService` that creates
// `request_sender_`.
const CreateRequestSenderCallback create_request_sender_callback_;
// Helper class that sends requests, handles retries and authentication.
std::unique_ptr<google_apis::RequestSender> request_sender_;
// Available courses for student role.
CourseListState student_courses_;
// All course work information grouped by course id.
CourseWorkPerCourse student_course_work_;
CourseWorkPerCourse teacher_course_work_;
// Fetch status of all student data.
FetchStatus student_data_fetch_status_ = FetchStatus::kNotFetched;
// Whether any of API requests made to fetch student data failed, indicating
// that student data may not be fully fresh.
bool student_data_fetch_had_failure_ = false;
// Pending callbacks awaiting all student data.
std::list<base::OnceClosure> callbacks_waiting_for_student_data_;
// Fetch status of all teacher data.
FetchStatus teacher_data_fetch_status_ = FetchStatus::kNotFetched;
// Whether any of API requests made to fetch teacher data failed, indicating
// that teacher data may not be fully fresh.
bool teacher_data_fetch_had_failure_ = false;
// The next available course work fetch request ID. The IDs will increase
// monotonically with each new request.
int next_course_work_request_id_ = 0;
// In progress course work requests, mapped by the course work request ID.
base::flat_map<int, std::unique_ptr<CourseWorkRequest>> course_work_requests_;
base::WeakPtrFactory<GlanceablesClassroomClientImpl> weak_factory_{this};
};
} // namespace ash
#endif // CHROME_BROWSER_UI_ASH_GLANCEABLES_GLANCEABLES_CLASSROOM_CLIENT_IMPL_H_