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