// 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.
#include "chrome/browser/ui/ash/glanceables/glanceables_classroom_client_impl.h"
#include <algorithm>
#include <functional>
#include <memory>
#include <numeric>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/glanceables/classroom/glanceables_classroom_types.h"
#include "ash/glanceables/glanceables_metrics.h"
#include "base/barrier_closure.h"
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/string_util.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/glanceables/glanceables_classroom_course_work_item.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "components/policy/content/policy_blocklist_service.h"
#include "components/policy/core/browser/url_blocklist_manager.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "google_apis/classroom/classroom_api_course_work_response_types.h"
#include "google_apis/classroom/classroom_api_courses_response_types.h"
#include "google_apis/classroom/classroom_api_list_course_work_request.h"
#include "google_apis/classroom/classroom_api_list_courses_request.h"
#include "google_apis/classroom/classroom_api_list_student_submissions_request.h"
#include "google_apis/classroom/classroom_api_student_submissions_response_types.h"
#include "google_apis/common/api_error_codes.h"
#include "google_apis/common/request_sender.h"
#include "google_apis/gaia/gaia_constants.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
namespace ash {
namespace {
using ::google_apis::ApiErrorCode;
using ::google_apis::RequestSender;
using ::google_apis::classroom::Course;
using ::google_apis::classroom::Courses;
using ::google_apis::classroom::CourseWork;
using ::google_apis::classroom::CourseWorkItem;
using ::google_apis::classroom::ListCoursesRequest;
using ::google_apis::classroom::ListCourseWorkRequest;
using ::google_apis::classroom::ListStudentSubmissionsRequest;
using ::google_apis::classroom::StudentSubmission;
using ::google_apis::classroom::StudentSubmissions;
// Special filter value for `ListCoursesRequest` to request courses with access
// limited to the requesting user.
constexpr char kOwnCoursesFilterValue[] = "me";
// Special parameter value to request student submissions for all course work in
// the specified course.
constexpr char kAllStudentSubmissionsParameterValue[] = "-";
constexpr char kClassroomUrl[] = "https://classroom.google.com/";
constexpr auto kCoursesCacheDuration = base::Hours(4);
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotationTag =
net::DefineNetworkTrafficAnnotation("glanceables_classroom_integration", R"(
semantics {
sender: "Glanceables keyed service"
description: "Provide ChromeOS users quick access to their "
"classroom items without opening the app or website"
trigger: "User presses the calendar pill in shelf, which triggers "
"opening the calendar, classroom (if available) and tasks "
"widgets. This specific client implementation "
"is responsible for fetching user's classroom data from "
"Google Classroom API."
internal {
contacts {
email: "[email protected]"
}
}
user_data {
type: ACCESS_TOKEN
}
data: "The request is authenticated with an OAuth2 access token "
"identifying the Google account"
destination: GOOGLE_OWNED_SERVICE
last_reviewed: "2023-08-21"
}
policy {
cookies_allowed: NO
setting: "This feature cannot be disabled in settings"
chrome_policy {
ContextualGoogleIntegrationsEnabled {
ContextualGoogleIntegrationsEnabled: false
}
}
}
)");
} // namespace
GlanceablesClassroomClientImpl::CourseListState::CourseListState(
base::Clock* clock)
: clock_(clock) {}
GlanceablesClassroomClientImpl::CourseListState::~CourseListState() = default;
bool GlanceablesClassroomClientImpl::CourseListState::
RunOrEnqueueCallbackAndUpdateFetchStatus(
GlanceablesClassroomClientImpl::FetchCoursesCallback callback) {
if (fetch_status_ == FetchStatus::kFetched &&
clock_->Now() - last_successful_fetch_time_ < kCoursesCacheDuration) {
std::move(callback).Run(/*success=*/true, courses_);
return false;
}
callbacks_.push_back(std::move(callback));
const bool needs_fetch = fetch_status_ != FetchStatus::kFetching;
fetch_status_ = FetchStatus::kFetching;
return needs_fetch;
}
void GlanceablesClassroomClientImpl::CourseListState::FinalizeFetch(
std::unique_ptr<CourseList> fetched_courses) {
const bool success = fetched_courses.get();
if (success) {
switch (fetch_status_) {
case FetchStatus::kNotFetched:
case FetchStatus::kFetched:
case FetchStatus::kFetchingInvalidated:
NOTREACHED_IN_MIGRATION();
break;
case FetchStatus::kFetching:
fetch_status_ = FetchStatus::kFetched;
break;
}
courses_.swap(*fetched_courses);
last_successful_fetch_time_ = clock_->Now();
} else {
fetch_status_ = FetchStatus::kNotFetched;
// NOTE: Keeping existing `courses_` state around, so it can be reused to
// surface some, potentially unfresh data to the user. Marking the list
// status as not fetched to force another fetch when courses get requested
// again.
}
RunCallbacks(success);
}
void GlanceablesClassroomClientImpl::CourseListState::RunCallbacks(
bool success) {
std::vector<FetchCoursesCallback> callbacks;
callbacks_.swap(callbacks);
for (auto& callback : callbacks) {
std::move(callback).Run(success, courses_);
}
}
GlanceablesClassroomClientImpl::CourseWorkRequest::CourseWorkRequest(
base::OnceClosure callback)
: callback_(std::move(callback)) {
CHECK(callback_);
}
GlanceablesClassroomClientImpl::CourseWorkRequest::~CourseWorkRequest() =
default;
void GlanceablesClassroomClientImpl::CourseWorkRequest::
IncrementPendingPageCount() {
++pending_page_requests_;
}
void GlanceablesClassroomClientImpl::CourseWorkRequest::
DecrementPendingPageCount() {
CHECK_GT(pending_page_requests_, 0);
--pending_page_requests_;
}
bool GlanceablesClassroomClientImpl::CourseWorkRequest::RespondIfComplete() {
CHECK(callback_);
if (pending_page_requests_ == 0) {
std::move(callback_).Run();
return true;
}
return false;
}
GlanceablesClassroomClientImpl::GlanceablesClassroomClientImpl(
Profile* profile,
base::Clock* clock,
const GlanceablesClassroomClientImpl::CreateRequestSenderCallback&
create_request_sender_callback)
: profile_(profile),
clock_(clock),
create_request_sender_callback_(create_request_sender_callback),
student_courses_(CourseListState(clock_)) {}
GlanceablesClassroomClientImpl::~GlanceablesClassroomClientImpl() = default;
bool GlanceablesClassroomClientImpl::IsDisabledByAdmin() const {
// 1) Check the pref.
const auto* const pref_service = profile_->GetPrefs();
if (!pref_service ||
!base::Contains(pref_service->GetList(
prefs::kContextualGoogleIntegrationsConfiguration),
prefs::kGoogleClassroomIntegrationName)) {
RecordContextualGoogleIntegrationStatus(
prefs::kGoogleClassroomIntegrationName,
ContextualGoogleIntegrationStatus::kDisabledByPolicy);
return true;
}
// 2) Check if the Classroom app is disabled by policy.
if (!apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(
profile_)) {
return true;
}
auto classroom_app_readiness = apps::Readiness::kUnknown;
apps::AppServiceProxyFactory::GetForProfile(profile_)
->AppRegistryCache()
.ForOneApp(web_app::kGoogleClassroomAppId,
[&classroom_app_readiness](const apps::AppUpdate& update) {
classroom_app_readiness = update.Readiness();
});
if (classroom_app_readiness == apps::Readiness::kDisabledByPolicy) {
RecordContextualGoogleIntegrationStatus(
prefs::kGoogleClassroomIntegrationName,
ContextualGoogleIntegrationStatus::kDisabledByAppBlock);
return true;
}
// 3) Check if the Classroom URL is blocked by policy.
const auto* const policy_blocklist_service =
PolicyBlocklistFactory::GetForBrowserContext(profile_);
if (!policy_blocklist_service ||
policy_blocklist_service->GetURLBlocklistState(GURL(kClassroomUrl)) ==
policy::URLBlocklist::URLBlocklistState::URL_IN_BLOCKLIST) {
RecordContextualGoogleIntegrationStatus(
prefs::kGoogleClassroomIntegrationName,
ContextualGoogleIntegrationStatus::kDisabledByUrlBlock);
return true;
}
RecordContextualGoogleIntegrationStatus(
prefs::kGoogleClassroomIntegrationName,
ContextualGoogleIntegrationStatus::kEnabled);
return false;
}
void GlanceablesClassroomClientImpl::IsStudentRoleActive(
IsRoleEnabledCallback callback) {
CHECK(callback);
FetchStudentCourses(base::BindOnce(
[](IsRoleEnabledCallback callback, bool success,
const CourseList& courses) {
const bool is_active = !courses.empty();
base::UmaHistogramBoolean(
"Ash.Glanceables.Api.Classroom.IsStudentRoleActiveResult",
is_active);
std::move(callback).Run(is_active);
},
std::move(callback)));
}
void GlanceablesClassroomClientImpl::GetCompletedStudentAssignments(
GetAssignmentsCallback callback) {
CHECK(callback);
auto due_predicate = base::BindRepeating(
[](const std::optional<base::Time>& due) { return true; });
auto submission_state_predicate =
base::BindRepeating([](GlanceablesClassroomStudentSubmissionState state) {
return state == GlanceablesClassroomStudentSubmissionState::kTurnedIn ||
state == GlanceablesClassroomStudentSubmissionState::kGraded;
});
auto sort_comparator =
base::BindRepeating([](const GlanceablesClassroomCourseWorkItem* lhs,
const GlanceablesClassroomCourseWorkItem* rhs) {
// Order by the submission's last update time in descending order.
return lhs->most_recent_submission_update_time() >
rhs->most_recent_submission_update_time();
});
InvokeOnceStudentDataFetched(base::BindOnce(
&GlanceablesClassroomClientImpl::GetFilteredStudentAssignments,
base::Unretained(this), std::move(due_predicate),
std::move(submission_state_predicate), std::move(sort_comparator),
std::move(callback)));
}
void GlanceablesClassroomClientImpl::
GetStudentAssignmentsWithApproachingDueDate(
GetAssignmentsCallback callback) {
CHECK(callback);
auto due_predicate = base::BindRepeating(
[](const base::Time& now, const std::optional<base::Time>& due) {
return due.has_value() && now < due.value();
},
clock_->Now());
auto submission_state_predicate =
base::BindRepeating([](GlanceablesClassroomStudentSubmissionState state) {
return state == GlanceablesClassroomStudentSubmissionState::kAssigned;
});
auto sort_comparator =
base::BindRepeating([](const GlanceablesClassroomCourseWorkItem* lhs,
const GlanceablesClassroomCourseWorkItem* rhs) {
// `due_predicate` should have filtered out items with no due date.
CHECK(lhs->due());
CHECK(rhs->due());
// Order by due date in ascending order.
return *lhs->due() < *rhs->due();
});
InvokeOnceStudentDataFetched(base::BindOnce(
&GlanceablesClassroomClientImpl::GetFilteredStudentAssignments,
base::Unretained(this), std::move(due_predicate),
std::move(submission_state_predicate), std::move(sort_comparator),
std::move(callback)));
}
void GlanceablesClassroomClientImpl::GetStudentAssignmentsWithMissedDueDate(
GetAssignmentsCallback callback) {
CHECK(callback);
auto due_predicate = base::BindRepeating(
[](const base::Time& now, const std::optional<base::Time>& due) {
return due.has_value() && now > due.value();
},
clock_->Now());
auto submission_state_predicate =
base::BindRepeating([](GlanceablesClassroomStudentSubmissionState state) {
return state == GlanceablesClassroomStudentSubmissionState::kAssigned;
});
auto sort_comparator =
base::BindRepeating([](const GlanceablesClassroomCourseWorkItem* lhs,
const GlanceablesClassroomCourseWorkItem* rhs) {
// `due_predicate` should have filtered out items with no due date.
CHECK(lhs->due());
CHECK(rhs->due());
// Order by due date in descending order.
return *lhs->due() > *rhs->due();
});
InvokeOnceStudentDataFetched(base::BindOnce(
&GlanceablesClassroomClientImpl::GetFilteredStudentAssignments,
base::Unretained(this), std::move(due_predicate),
std::move(submission_state_predicate), std::move(sort_comparator),
std::move(callback)));
}
void GlanceablesClassroomClientImpl::GetStudentAssignmentsWithoutDueDate(
GetAssignmentsCallback callback) {
CHECK(callback);
auto due_predicate = base::BindRepeating(
[](const std::optional<base::Time>& due) { return !due.has_value(); });
auto submission_state_predicate =
base::BindRepeating([](GlanceablesClassroomStudentSubmissionState state) {
return state == GlanceablesClassroomStudentSubmissionState::kAssigned;
});
auto sort_comparator =
base::BindRepeating([](const GlanceablesClassroomCourseWorkItem* lhs,
const GlanceablesClassroomCourseWorkItem* rhs) {
// Order by course work item creation time in descending order.
return lhs->creation_time() > rhs->creation_time();
});
InvokeOnceStudentDataFetched(base::BindOnce(
&GlanceablesClassroomClientImpl::GetFilteredStudentAssignments,
base::Unretained(this), std::move(due_predicate),
std::move(submission_state_predicate), std::move(sort_comparator),
std::move(callback)));
}
void GlanceablesClassroomClientImpl::OnGlanceablesBubbleClosed() {
for (FetchStatus* fetch_status :
{&student_data_fetch_status_, &teacher_data_fetch_status_}) {
InvalidateFetchStatus(fetch_status);
}
}
// static
void GlanceablesClassroomClientImpl::InvalidateFetchStatus(
FetchStatus* fetch_status) {
switch (*fetch_status) {
case FetchStatus::kNotFetched:
break;
case FetchStatus::kFetched:
*fetch_status = FetchStatus::kNotFetched;
break;
case FetchStatus::kFetching:
*fetch_status = FetchStatus::kFetchingInvalidated;
break;
case FetchStatus::kFetchingInvalidated:
// Do no restart fetch if it's still in progress, which could happen if
// the user toggles glanceables bubble in quick succession.
*fetch_status = FetchStatus::kFetching;
break;
}
}
void GlanceablesClassroomClientImpl::FetchStudentCourses(
FetchCoursesCallback callback) {
CHECK(callback);
const bool needs_refetch =
student_courses_.RunOrEnqueueCallbackAndUpdateFetchStatus(
std::move(callback));
if (needs_refetch) {
auto courses = std::make_unique<CourseList>();
FetchCoursesPage(/*page_token=*/"", std::move(courses));
}
}
bool GlanceablesClassroomClientImpl::ShouldFetchSubmissionsPerCourseWork(
CourseWorkType course_work_type) const {
return course_work_type == CourseWorkType::kTeacher;
}
GlanceablesClassroomClientImpl::CourseWorkPerCourse&
GlanceablesClassroomClientImpl::GetCourseWork(CourseWorkType type) {
return type == CourseWorkType::kStudent ? student_course_work_
: teacher_course_work_;
}
void GlanceablesClassroomClientImpl::SetCourseWorkFetchHadFailure(
CourseWorkType type) {
switch (type) {
case CourseWorkType::kStudent:
student_data_fetch_had_failure_ = true;
break;
case CourseWorkType::kTeacher:
teacher_data_fetch_had_failure_ = true;
break;
}
}
void GlanceablesClassroomClientImpl::FetchCourseWork(
const std::string& course_id,
CourseWorkType course_work_type,
base::OnceClosure callback) {
CHECK(!course_id.empty());
CHECK(callback);
const int request_id = next_course_work_request_id_++;
const auto [request_it, request_inserted] = course_work_requests_.emplace(
request_id, std::make_unique<CourseWorkRequest>(std::move(callback)));
CHECK(request_inserted);
auto& course_work = GetCourseWork(course_work_type);
for (auto& course_work_info : course_work[course_id]) {
course_work_info.second.InvalidateCourseWorkItem();
}
FetchCourseWorkPage(request_id, course_id, /*page_token=*/"",
/*page_number=*/1, course_work_type);
}
void GlanceablesClassroomClientImpl::FetchStudentSubmissions(
const std::string& course_id,
const std::string& course_work_id,
CourseWorkType course_work_type,
base::OnceClosure callback) {
CHECK(!course_id.empty());
CHECK(callback);
CourseWorkPerCourse& course_work = GetCourseWork(course_work_type);
// Invalidate any preexisting cached student submissions info for requested
// course work ID.
if (course_work_id == kAllStudentSubmissionsParameterValue) {
for (auto& course_work_info : course_work[course_id]) {
course_work_info.second.InvalidateStudentSubmissions();
}
} else {
course_work[course_id][course_work_id].InvalidateStudentSubmissions();
}
FetchStudentSubmissionsPage(course_id, course_work_id,
/*page_token=*/"", /*page_number=*/1,
course_work_type, std::move(callback));
}
void GlanceablesClassroomClientImpl::InvokeOnceStudentDataFetched(
base::OnceClosure callback) {
CHECK(callback);
if (student_data_fetch_status_ == FetchStatus::kFetched) {
std::move(callback).Run();
return;
}
callbacks_waiting_for_student_data_.push_back(std::move(callback));
const bool needs_fetch =
student_data_fetch_status_ != FetchStatus::kFetching &&
student_data_fetch_status_ != FetchStatus::kFetchingInvalidated;
student_data_fetch_status_ = FetchStatus::kFetching;
if (needs_fetch) {
student_data_fetch_had_failure_ = false;
FetchStudentCourses(base::BindOnce(
&GlanceablesClassroomClientImpl::OnCoursesFetched,
weak_factory_.GetWeakPtr(), CourseWorkType::kStudent,
base::BindOnce(&GlanceablesClassroomClientImpl::OnStudentDataFetched,
weak_factory_.GetWeakPtr(), clock_->Now())));
}
}
void GlanceablesClassroomClientImpl::FetchCoursesPage(
const std::string& page_token,
std::unique_ptr<CourseList> fetched_courses) {
auto* const request_sender = GetRequestSender();
request_sender->StartRequestWithAuthRetry(
std::make_unique<ListCoursesRequest>(
request_sender, /*student_id=*/kOwnCoursesFilterValue,
/*teacher_id=*/"", page_token,
base::BindOnce(&GlanceablesClassroomClientImpl::OnCoursesPageFetched,
weak_factory_.GetWeakPtr(), std::move(fetched_courses),
clock_->Now())));
}
void GlanceablesClassroomClientImpl::OnCoursesPageFetched(
std::unique_ptr<CourseList> fetched_courses,
const base::Time& request_start_time,
base::expected<std::unique_ptr<Courses>, ApiErrorCode> result) {
base::UmaHistogramTimes("Ash.Glanceables.Api.Classroom.GetCourses.Latency",
clock_->Now() - request_start_time);
base::UmaHistogramSparse("Ash.Glanceables.Api.Classroom.GetCourses.Status",
result.error_or(ApiErrorCode::HTTP_SUCCESS));
if (!result.has_value()) {
student_courses_.FinalizeFetch(nullptr);
return;
}
for (const auto& item : result.value()->items()) {
if (item->state() == Course::State::kActive) {
fetched_courses->push_back(std::make_unique<GlanceablesClassroomCourse>(
item->id(), item->name()));
}
}
if (result.value()->next_page_token().empty()) {
base::UmaHistogramCounts100(
"Ash.Glanceables.Api.Classroom.StudentCoursesCount",
fetched_courses->size());
student_courses_.FinalizeFetch(std::move(fetched_courses));
} else {
FetchCoursesPage(result.value()->next_page_token(),
std::move(fetched_courses));
}
}
void GlanceablesClassroomClientImpl::OnCoursesFetched(
CourseWorkType course_work_type,
base::OnceClosure on_course_work_and_student_submissions_fetched,
bool success,
const CourseList& course_list) {
CHECK(on_course_work_and_student_submissions_fetched);
if (!success) {
SetCourseWorkFetchHadFailure(course_work_type);
}
const bool fetch_submissions_per_course_work =
ShouldFetchSubmissionsPerCourseWork(course_work_type);
// `FetchCourseWork()` + `FetchStudentSubmissions()` per course.
const auto expected_callback_calls =
course_list.size() * (fetch_submissions_per_course_work ? 1 : 2);
const auto barrier_closure = base::BarrierClosure(
expected_callback_calls,
std::move(on_course_work_and_student_submissions_fetched));
for (const auto& course : course_list) {
FetchCourseWork(course->id, course_work_type, barrier_closure);
if (!fetch_submissions_per_course_work) {
FetchStudentSubmissions(course->id, kAllStudentSubmissionsParameterValue,
course_work_type, barrier_closure);
}
}
}
void GlanceablesClassroomClientImpl::FetchCourseWorkPage(
int request_id,
const std::string& course_id,
const std::string& page_token,
int page_number,
CourseWorkType course_work_type) {
CHECK(!course_id.empty());
auto request_it = course_work_requests_.find(request_id);
if (request_it == course_work_requests_.end() || !request_it->second) {
return;
}
request_it->second->IncrementPendingPageCount();
auto* const request_sender = GetRequestSender();
request_sender->StartRequestWithAuthRetry(
std::make_unique<ListCourseWorkRequest>(
request_sender, course_id, page_token,
base::BindOnce(
&GlanceablesClassroomClientImpl::OnCourseWorkPageFetched,
weak_factory_.GetWeakPtr(), request_id, course_id,
course_work_type, clock_->Now(), page_number)));
}
void GlanceablesClassroomClientImpl::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<CourseWork>, ApiErrorCode> result) {
CHECK(!course_id.empty());
base::UmaHistogramTimes("Ash.Glanceables.Api.Classroom.GetCourseWork.Latency",
clock_->Now() - request_start_time);
base::UmaHistogramSparse("Ash.Glanceables.Api.Classroom.GetCourseWork.Status",
result.error_or(ApiErrorCode::HTTP_SUCCESS));
auto request_it = course_work_requests_.find(request_id);
if (request_it == course_work_requests_.end() || !request_it->second) {
return;
}
CourseWorkPerCourse& course_work = GetCourseWork(course_work_type);
CourseWorkInfo& course_work_for_course = course_work[course_id];
if (!result.has_value()) {
SetCourseWorkFetchHadFailure(course_work_type);
for (auto& course_work_item : course_work_for_course) {
course_work_item.second.RevalidateCourseWorkItem();
}
request_it->second->DecrementPendingPageCount();
if (request_it->second->RespondIfComplete()) {
course_work_requests_.erase(request_it);
}
return;
}
const bool fetch_submissions =
ShouldFetchSubmissionsPerCourseWork(course_work_type);
std::set<std::string> submissions_to_fetch;
for (const auto& item : result.value()->items()) {
if (item->state() != CourseWorkItem::State::kPublished) {
course_work_for_course.erase(item->id());
continue;
}
auto& course_work_item = course_work_for_course[item->id()];
course_work_item.SetCourseWorkItem(item.get());
if (fetch_submissions) {
if (course_work_item.StudentSubmissionsNeedRefetch(clock_->Now())) {
submissions_to_fetch.insert(item->id());
} else {
course_work_item.SetHasFreshSubmissionsState(false, clock_->Now());
}
}
}
if (!result.value()->next_page_token().empty()) {
FetchCourseWorkPage(request_id, course_id,
result.value()->next_page_token(), page_number + 1,
course_work_type);
} else {
base::UmaHistogramCounts100(
"Ash.Glanceables.Api.Classroom.GetCourseWork.PagesCount", page_number);
}
// NOTE: If `submissions_to_fetch` is empty, `barrier_closure` will run
// immediately.
const auto barrier_closure = base::BarrierClosure(
submissions_to_fetch.size(),
base::BindOnce(
&GlanceablesClassroomClientImpl::OnCourseWorkSubmissionsFetched,
weak_factory_.GetWeakPtr(), request_id, course_id));
for (const auto& course_work_id : submissions_to_fetch) {
FetchStudentSubmissions(course_id, course_work_id, course_work_type,
barrier_closure);
}
}
void GlanceablesClassroomClientImpl::OnCourseWorkSubmissionsFetched(
int request_id,
const std::string& course_id) {
auto request_it = course_work_requests_.find(request_id);
if (request_it == course_work_requests_.end() || !request_it->second) {
return;
}
request_it->second->DecrementPendingPageCount();
if (request_it->second->RespondIfComplete()) {
course_work_requests_.erase(request_it);
}
}
void GlanceablesClassroomClientImpl::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) {
CHECK(!course_id.empty());
CHECK(callback);
auto* const request_sender = GetRequestSender();
request_sender->StartRequestWithAuthRetry(
std::make_unique<ListStudentSubmissionsRequest>(
request_sender, course_id, course_work_id, page_token,
base::BindOnce(
&GlanceablesClassroomClientImpl::OnStudentSubmissionsPageFetched,
weak_factory_.GetWeakPtr(), course_id, course_work_id,
course_work_type, clock_->Now(), page_number,
std::move(callback))));
}
void GlanceablesClassroomClientImpl::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<StudentSubmissions>, ApiErrorCode> result) {
CHECK(!course_id.empty());
CHECK(callback);
base::UmaHistogramTimes(
"Ash.Glanceables.Api.Classroom.GetStudentSubmissions.Latency",
clock_->Now() - request_start_time);
base::UmaHistogramSparse(
"Ash.Glanceables.Api.Classroom.GetStudentSubmissions.Status",
result.error_or(ApiErrorCode::HTTP_SUCCESS));
CourseWorkPerCourse& course_work = GetCourseWork(course_work_type);
auto& course_work_info_map = course_work[course_id];
GlanceablesClassroomCourseWorkItem* const shared_course_work_info =
(course_work_id == kAllStudentSubmissionsParameterValue)
? nullptr
: &course_work_info_map[course_work_id];
if (!result.has_value()) {
SetCourseWorkFetchHadFailure(course_work_type);
// On failure, restore the student submissions state for all course work
// item that are in scope for the fetch.
if (course_work_id == kAllStudentSubmissionsParameterValue) {
for (auto& course_work_info : course_work_info_map) {
course_work_info.second.RestorePreviousStudentSubmissions();
}
} else {
shared_course_work_info->RestorePreviousStudentSubmissions();
}
std::move(callback).Run();
return;
}
for (const auto& item : result.value()->items()) {
if (shared_course_work_info) {
CHECK_EQ(course_work_id, item->course_work_id());
}
GlanceablesClassroomCourseWorkItem* const submission_course_work_info =
shared_course_work_info ? shared_course_work_info
: &course_work_info_map[item->course_work_id()];
submission_course_work_info->AddStudentSubmission(item.get());
}
if (result.value()->next_page_token().empty()) {
base::UmaHistogramCounts100(
"Ash.Glanceables.Api.Classroom.GetStudentSubmissions.PagesCount",
page_number);
if (shared_course_work_info) {
shared_course_work_info->SetHasFreshSubmissionsState(true, clock_->Now());
}
std::move(callback).Run();
} else {
FetchStudentSubmissionsPage(
course_id, course_work_id, result.value()->next_page_token(),
page_number + 1, course_work_type, std::move(callback));
}
}
void GlanceablesClassroomClientImpl::OnStudentDataFetched(
const base::Time& sequence_start_time) {
base::UmaHistogramMediumTimes(
"Ash.Glanceables.Api.Classroom.StudentDataFetchTime",
clock_->Now() - sequence_start_time);
if (student_data_fetch_had_failure_) {
student_data_fetch_status_ = FetchStatus::kNotFetched;
} else {
switch (student_data_fetch_status_) {
case FetchStatus::kNotFetched:
case FetchStatus::kFetched:
NOTREACHED_IN_MIGRATION();
break;
case FetchStatus::kFetching:
student_data_fetch_status_ = FetchStatus::kFetched;
break;
case FetchStatus::kFetchingInvalidated:
student_data_fetch_status_ = FetchStatus::kNotFetched;
break;
}
}
PruneInvalidCourseWork(student_courses_.courses(), student_course_work_);
if (!student_data_fetch_had_failure_) {
for (const auto& course : student_courses_.courses()) {
const auto iter = student_course_work_.find(course->id);
if (iter == student_course_work_.end()) {
continue;
}
base::UmaHistogramCounts1000(
"Ash.Glanceables.Api.Classroom.CourseWorkItemsPerStudentCourseCount",
iter->second.size());
base::UmaHistogramCounts1000(
"Ash.Glanceables.Api.Classroom."
"StudentSubmissionsPerStudentCourseCount",
std::accumulate(iter->second.begin(), iter->second.end(), 0,
[](int count, const auto& x) {
return count + x.second.total_submissions();
}));
}
}
std::list<base::OnceClosure> callbacks;
callbacks_waiting_for_student_data_.swap(callbacks);
for (auto& cb : callbacks) {
std::move(cb).Run();
}
}
void GlanceablesClassroomClientImpl::GetFilteredStudentAssignments(
base::RepeatingCallback<bool(const std::optional<base::Time>&)>
due_predicate,
base::RepeatingCallback<bool(GlanceablesClassroomStudentSubmissionState)>
submission_state_predicate,
SortComparator sort_comparator,
GetAssignmentsCallback callback) {
CHECK(due_predicate);
CHECK(submission_state_predicate);
CHECK(callback);
if (callback.IsCancelled()) {
return;
}
using CourseNameAndCourseWork =
std::pair<std::string, const GlanceablesClassroomCourseWorkItem*>;
std::vector<CourseNameAndCourseWork> filtered_items;
for (const auto& course : student_courses_.courses()) {
const auto course_work_iter = student_course_work_.find(course->id);
if (course_work_iter == student_course_work_.end()) {
continue;
}
for (const auto& course_work_item : course_work_iter->second) {
if (!course_work_item.second.SatisfiesPredicates(
due_predicate, submission_state_predicate)) {
continue;
}
filtered_items.push_back(
std::make_pair(course->name, &course_work_item.second));
}
}
std::sort(filtered_items.begin(), filtered_items.end(),
[&sort_comparator](const CourseNameAndCourseWork& lhs,
const CourseNameAndCourseWork& rhs) {
return sort_comparator.Run(lhs.second, rhs.second);
});
std::vector<std::unique_ptr<GlanceablesClassroomAssignment>>
filtered_assignments;
for (const auto& item : filtered_items) {
filtered_assignments.push_back(item.second->CreateClassroomAssignment(
item.first, /*include_aggregated_submissions_state=*/false));
}
std::move(callback).Run(!student_data_fetch_had_failure_,
std::move(filtered_assignments));
}
void GlanceablesClassroomClientImpl::PruneInvalidCourseWork(
const CourseList& courses,
CourseWorkPerCourse& course_work) {
std::set<std::string> course_ids;
for (const auto& course : courses) {
course_ids.insert(course->id);
}
base::EraseIf(course_work, [&course_ids](const auto& per_course_info) {
return !course_ids.contains(per_course_info.first);
});
for (const auto& course : courses) {
const auto course_work_iter = course_work.find(course->id);
if (course_work_iter != course_work.end()) {
base::EraseIf(course_work_iter->second, [](const auto& course_work_item) {
return !course_work_item.second.IsValid();
});
}
}
}
RequestSender* GlanceablesClassroomClientImpl::GetRequestSender() {
if (!request_sender_) {
CHECK(create_request_sender_callback_);
request_sender_ =
std::move(create_request_sender_callback_)
.Run(/*scopes=*/
{
GaiaConstants::kClassroomReadOnlyCoursesOAuth2Scope,
GaiaConstants::kClassroomReadOnlyCourseWorkSelfOAuth2Scope,
GaiaConstants::
kClassroomReadOnlyStudentSubmissionsSelfOAuth2Scope,
},
kTrafficAnnotationTag);
CHECK(request_sender_);
}
return request_sender_.get();
}
} // namespace ash