0

Add up next view

This CL introduces an "Up next" view in the CalendarView where users can see their upcoming events.

Screenshot: http://shortn/_qA0fSSZL56

Smaller refactors include:
- Reduce CalendarEventListItemViewJelly controller dependency so it can be re-used in the UpNext view.
- LayoutManager resizes CalendarView scroll view to dynamically make space for the CalendarUpNextView.

Bug: b:258165425
Change-Id: I478bd4a5f33c222eaec0a053ac4e949f4bd9e18f
Low-Coverage-Reason: Low in existing class, just made a small refactor wrapping existing params in a struct, no new functionality added
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4013467
Commit-Queue: Sam Cackett <samcackett@google.com>
Reviewed-by: Jiaming Cheng <jiamingc@chromium.org>
Auto-Submit: Sam Cackett <samcackett@google.com>
Cr-Commit-Position: refs/heads/main@{#1072224}
This commit is contained in:
Sam Cackett
2022-11-16 15:15:43 +00:00
committed by Chromium LUCI CQ
parent c8131acb55
commit f4c17f031b
18 changed files with 710 additions and 23 deletions

@ -1726,6 +1726,8 @@ component("ash") {
"system/time/calendar_model.h",
"system/time/calendar_month_view.cc",
"system/time/calendar_month_view.h",
"system/time/calendar_up_next_view.cc",
"system/time/calendar_up_next_view.h",
"system/time/calendar_utils.cc",
"system/time/calendar_utils.h",
"system/time/calendar_view.cc",
@ -3058,6 +3060,7 @@ test("ash_unittests") {
"system/time/calendar_month_view_unittest.cc",
"system/time/calendar_unittest_utils.cc",
"system/time/calendar_unittest_utils.h",
"system/time/calendar_up_next_view_unittest.cc",
"system/time/calendar_utils_unittest.cc",
"system/time/calendar_view_controller_unittest.cc",
"system/time/calendar_view_unittest.cc",

@ -4199,6 +4199,10 @@ Connect your device to power.
Open in Google Calendar
</message>
<message name="IDS_ASH_CALENDAR_UP_NEXT" desc="Header label for the Calendar Up next view.">
Up next
</message>
<message name="IDS_ASH_STATUS_TRAY_PROGRESS_BAR_ACCESSIBLE_NAME" desc="The accessible name for the progress bar shown in the status tray.">
Loading
</message>

@ -0,0 +1 @@
05da4a226414b5fea76a977dcf45df6a3272546e

@ -90,7 +90,8 @@ class CalendarEventListItemDot : public views::View {
// Creates and returns a label containing the event summary.
views::Builder<views::Label> CreateSummaryLabel(
const std::string& event_summary,
const std::u16string& tooltip_text) {
const std::u16string& tooltip_text,
const int& max_width) {
return views::Builder<views::Label>(
bubble_utils::CreateLabel(
bubble_utils::TypographyStyle::kButton1,
@ -100,6 +101,9 @@ views::Builder<views::Label> CreateSummaryLabel(
.SetID(kSummaryLabelID)
.SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT)
.SetAutoColorReadabilityEnabled(false)
.SetMultiLine(true)
.SetMaxLines(1)
.SetMaximumWidth(max_width)
.SetElideBehavior(gfx::ElideBehavior::ELIDE_TAIL)
.SetSubpixelRenderingEnabled(false)
.SetTextContext(CONTEXT_CALENDAR_DATE)
@ -126,20 +130,21 @@ views::Builder<views::Label> CreateTimeLabel(
CalendarEventListItemViewJelly::CalendarEventListItemViewJelly(
CalendarViewController* calendar_view_controller,
SelectedDateParams selected_date_params,
google_apis::calendar::CalendarEvent event,
const bool round_top_corners,
const bool round_bottom_corners)
const bool round_bottom_corners,
const int max_width)
: ActionableView(TrayPopupInkDropStyle::FILL_BOUNDS),
calendar_view_controller_(calendar_view_controller),
selected_date_params_(selected_date_params),
event_url_(event.html_link()) {
SetLayoutManager(std::make_unique<views::FillLayout>());
DCHECK(calendar_view_controller_->selected_date().has_value());
const auto [start_time, end_time] = calendar_utils::GetStartAndEndTime(
&event, calendar_view_controller->selected_date().value(),
calendar_view_controller->selected_date_midnight(),
calendar_view_controller->selected_date_midnight_utc());
&event, selected_date_params_.selected_date,
selected_date_params_.selected_date_midnight,
selected_date_params_.selected_date_midnight_utc);
const auto [start_time_accessible_name, end_time_accessible_name] =
event_date_formatter_util::GetStartAndEndTimeAccessibleNames(start_time,
end_time);
@ -169,8 +174,8 @@ CalendarEventListItemViewJelly::CalendarEventListItemViewJelly(
std::u16string formatted_time_text;
if (calendar_utils::IsMultiDayEvent(&event) || event.all_day_event()) {
formatted_time_text = event_date_formatter_util::GetMultiDayText(
&event, calendar_view_controller->selected_date_midnight(),
calendar_view_controller->selected_date_midnight_utc());
&event, selected_date_params_.selected_date_midnight,
selected_date_params_.selected_date_midnight_utc);
} else {
formatted_time_text =
event_date_formatter_util::GetFormattedInterval(start_time, end_time);
@ -188,7 +193,8 @@ CalendarEventListItemViewJelly::CalendarEventListItemViewJelly(
views::Builder<views::View>()
.SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical))
.AddChild(CreateSummaryLabel(event.summary(), tooltip_text))
.AddChild(CreateSummaryLabel(event.summary(), tooltip_text,
max_width))
.AddChild(CreateTimeLabel(formatted_time_text, tooltip_text)))
.Build());
}
@ -209,10 +215,9 @@ bool CalendarEventListItemViewJelly::PerformAction(const ui::Event& event) {
GURL finalized_url;
bool opened_pwa = false;
DCHECK(calendar_view_controller_->selected_date().has_value());
Shell::Get()->system_tray_model()->client()->ShowCalendarEvent(
event_url_, calendar_view_controller_->selected_date_midnight(),
opened_pwa, finalized_url);
event_url_, selected_date_params_.selected_date_midnight, opened_pwa,
finalized_url);
return true;
}

@ -23,6 +23,12 @@ constexpr int kTimeLabelID = 101;
class CalendarViewController;
struct SelectedDateParams {
base::Time selected_date;
base::Time selected_date_midnight;
base::Time selected_date_midnight_utc;
};
// This view displays a jelly version of a calendar event entry.
class ASH_EXPORT CalendarEventListItemViewJelly : public ActionableView {
public:
@ -30,9 +36,11 @@ class ASH_EXPORT CalendarEventListItemViewJelly : public ActionableView {
CalendarEventListItemViewJelly(
CalendarViewController* calendar_view_controller,
SelectedDateParams selected_date_params,
google_apis::calendar::CalendarEvent event,
const bool round_top_corners,
const bool round_bottom_corners);
const bool round_bottom_corners,
const int max_width = 0);
CalendarEventListItemViewJelly(const CalendarEventListItemViewJelly& other) =
delete;
CalendarEventListItemViewJelly& operator=(
@ -51,6 +59,8 @@ class ASH_EXPORT CalendarEventListItemViewJelly : public ActionableView {
// Unowned.
CalendarViewController* const calendar_view_controller_;
const SelectedDateParams selected_date_params_;
// The URL for the meeting event.
const GURL event_url_;
};

@ -53,13 +53,18 @@ class CalendarViewEventListItemViewJellyTest : public AshTestBase {
void CreateEventListItemView(base::Time date,
google_apis::calendar::CalendarEvent* event,
bool round_top_corners = false,
bool round_bottom_corners = false) {
bool round_bottom_corners = false,
int max_width = 0) {
event_list_item_view_jelly_.reset();
controller_->UpdateMonth(date);
controller_->selected_date_ = date;
event_list_item_view_jelly_ =
std::make_unique<CalendarEventListItemViewJelly>(
controller_.get(), *event, round_top_corners, round_bottom_corners);
controller_.get(),
SelectedDateParams{controller_->selected_date().value(),
controller_->selected_date_midnight(),
controller_->selected_date_midnight_utc()},
*event, round_top_corners, round_bottom_corners, max_width);
}
void SetSelectedDateInController(base::Time date) {
@ -167,4 +172,28 @@ TEST_F(CalendarViewEventListItemViewJellyTest, AllRoundedCorners) {
background_layer->rounded_corner_radii());
}
TEST_F(CalendarViewEventListItemViewJellyTest, MaxLabelWidth) {
base::Time date;
ASSERT_TRUE(base::Time::FromString("22 Nov 2021 00:00 UTC", &date));
SetSelectedDateInController(date);
const char* start_time_string = "22 Nov 2021 09:00 GMT";
const char* end_time_string = "22 Nov 2021 10:00 GMT";
const auto event = CreateEvent(start_time_string, end_time_string);
// If we don't set `max_width`, it should default to 0 (which the
// `views::Label`) will ignore).
CreateEventListItemView(date, event.get(), /*round_top_corners*/ true,
/*round_bottom_corners*/ true);
EXPECT_EQ(GetSummaryLabel()->GetMaximumWidth(), 0);
// If we set a `max_width`, it should exist on the Summary Label.
const auto max_width = 200;
CreateEventListItemView(date, event.get(), /*round_top_corners*/ true,
/*round_bottom_corners*/ true,
/*max_width=*/max_width);
EXPECT_EQ(GetSummaryLabel()->GetMaximumWidth(), 200);
}
} // namespace ash

@ -232,7 +232,12 @@ std::unique_ptr<views::View> CalendarEventListView::CreateChildEventListView(
++it) {
container->AddChildView(std::make_unique<CalendarEventListItemViewJelly>(
/*calendar_view_controller=*/calendar_view_controller_,
/*event=*/*it,
/*selected_date_params=*/
SelectedDateParams{
calendar_view_controller_->selected_date().value(),
calendar_view_controller_->selected_date_midnight(),
calendar_view_controller_->selected_date_midnight_utc()}, /*event=*/
*it,
/*round_top_corners=*/it == events.begin(),
/*round_bottom_corners=*/it->id() == events.rbegin()->id()));
}

@ -89,6 +89,45 @@ void SortByDateAscending(
});
}
bool EventStartsInTenMins(const CalendarEvent& event,
const base::Time& now_local) {
const int start_time_difference_in_mins =
(ash::calendar_utils::GetStartTimeAdjusted(&event) - now_local)
.InMinutes();
return (0 <= start_time_difference_in_mins &&
start_time_difference_in_mins <= 10);
}
bool EventStartedLessThanOneHourAgo(const CalendarEvent& event,
const base::Time& now_local) {
const int start_time_difference_in_mins =
(ash::calendar_utils::GetStartTimeAdjusted(&event) - now_local)
.InMinutes();
const int end_time_difference_in_mins =
(ash::calendar_utils::GetEndTimeAdjusted(&event) - now_local).InMinutes();
return (0 <= end_time_difference_in_mins &&
0 > start_time_difference_in_mins &&
start_time_difference_in_mins >= -60);
}
// Returns events that start in 10 minutes time, or events that are in progress
// and started less than one hour ago.
auto FilterEventsStartingSoonOrRecentlyInProgress(
const ash::SingleDayEventList& list,
const base::Time& now_local) {
std::list<CalendarEvent> result;
for (const CalendarEvent& event : list) {
if (EventStartsInTenMins(event, now_local) ||
EventStartedLessThanOneHourAgo(event, now_local))
result.emplace_back(event);
}
return result;
}
} // namespace
namespace ash {
@ -479,6 +518,12 @@ CalendarModel::FindEventsSplitByMultiDayAndSameDay(base::Time day) const {
return SplitEventsIntoMultiDayAndSameDay(FindEvents(day));
}
std::list<CalendarEvent> CalendarModel::FindUpcomingEvents(
base::Time now_local) const {
return FilterEventsStartingSoonOrRecentlyInProgress(FindEvents(now_local),
now_local);
}
CalendarModel::FetchingStatus CalendarModel::FindFetchingStatus(
base::Time start_time) const {
if (!calendar_utils::ShouldFetchEvents())

@ -121,6 +121,11 @@ class ASH_EXPORT CalendarModel : public SessionObserver {
std::tuple<SingleDayEventList, SingleDayEventList>
FindEventsSplitByMultiDayAndSameDay(base::Time day) const;
// Uses the `FindEvents` method to get events for that day and then filters
// the result into events that start or end in the next two hours.
std::list<google_apis::calendar::CalendarEvent> FindUpcomingEvents(
base::Time now_local) const;
// Checks the `FetchingStatus` of a given start time.
FetchingStatus FindFetchingStatus(base::Time start_time) const;
@ -139,6 +144,8 @@ class ASH_EXPORT CalendarModel : public SessionObserver {
friend class CalendarViewAnimationTest;
friend class CalendarViewEventListViewTest;
friend class CalendarViewTest;
friend class CalendarViewWithJellyEnabledTest;
friend class CalendarUpNextViewTest;
friend class GlanceablesTest;
// Checks if the event has allowed statuses and is eligible for insertion.

@ -1243,4 +1243,78 @@ TEST_F(CalendarModelTest, FindEventsSplitByMultiDayAndSameDay) {
EXPECT_EQ(same_day_events.back().id(), kSameDayId);
}
TEST_F(CalendarModelTest, FindUpcomingEvents) {
// Set timezone and fake now.
const char* kNow = "10 Nov 2022 13:00 GMT";
ash::system::ScopedTimezoneSettings timezone_settings(u"GMT");
SetTodayFromStr(kNow);
const char* kSummary = "summary";
const char* kEventStartingInTenMinsId = "event_starting_in_ten_mins";
const char* kEventStartingInThirtyMinsId = "event_starting_in_thirty_mins";
const char* kEventStartingInTwoHoursId = "event_starting_in_two_hours";
const char* kEventInProgressStartedLessThanOneHourAgoId =
"event_in_progress_started_less_than_one_hour_ago";
const char* kEventInProgressStartedMoreThanOneHourAgoId =
"event_in_progress_started_more_than_one_hour_ago";
const char* kEventFinishedId = "event_finished";
auto event_starting_in_ten_mins = calendar_test_utils::CreateEvent(
kEventStartingInTenMinsId, kSummary, "10 Nov 2022 13:10 GMT",
"10 Nov 2022 15:00 GMT");
auto event_starting_in_thirty_mins = calendar_test_utils::CreateEvent(
kEventStartingInThirtyMinsId, kSummary, "10 Nov 2022 13:30 GMT",
"10 Nov 2022 15:00 GMT");
auto event_starting_in_two_hours = calendar_test_utils::CreateEvent(
kEventStartingInTwoHoursId, kSummary, "10 Nov 2022 15:00 GMT",
"10 Nov 2022 16:00 GMT");
auto event_in_progress_started_less_than_one_hour_ago =
calendar_test_utils::CreateEvent(
kEventInProgressStartedLessThanOneHourAgoId, kSummary,
"10 Nov 2022 12:01:00 GMT", "10 Nov 2022 17:00 GMT");
auto event_in_progress_started_more_than_one_hour_ago =
calendar_test_utils::CreateEvent(
kEventInProgressStartedMoreThanOneHourAgoId, kSummary,
"10 Nov 2022 11:00 GMT", "10 Nov 2022 17:00 GMT");
auto event_finished = calendar_test_utils::CreateEvent(
kEventFinishedId, kSummary, "10 Nov 2022 12:30 GMT",
"10 Nov 2022 12:59 GMT");
// Prepare mock events list.
std::unique_ptr<google_apis::calendar::EventList> event_list =
std::make_unique<google_apis::calendar::EventList>();
event_list->InjectItemForTesting(std::move(event_starting_in_ten_mins));
event_list->InjectItemForTesting(std::move(event_starting_in_thirty_mins));
event_list->InjectItemForTesting(std::move(event_starting_in_two_hours));
event_list->InjectItemForTesting(
std::move(event_in_progress_started_less_than_one_hour_ago));
event_list->InjectItemForTesting(
std::move(event_in_progress_started_more_than_one_hour_ago));
event_list->InjectItemForTesting(std::move(event_finished));
// Mock the events are fetched.
MockOnEventsFetched(calendar_utils::GetStartOfMonthUTC(
calendar_test_utils::GetTimeFromString(kNow)),
google_apis::ApiErrorCode::HTTP_SUCCESS,
event_list.get());
auto events = calendar_model_->FindUpcomingEvents(now_);
auto event_list_contains = [](auto& event_list, auto& id) {
return base::Contains(event_list, id, &CalendarEvent::id);
};
// We should only get the 2 events back that start in 10 mins or were ongoing
// with < 60 mins passed.
EXPECT_EQ(events.size(), size_t(2));
EXPECT_TRUE(event_list_contains(events, kEventStartingInTenMinsId));
EXPECT_FALSE(event_list_contains(events, kEventStartingInThirtyMinsId));
EXPECT_FALSE(event_list_contains(events, kEventStartingInTwoHoursId));
EXPECT_TRUE(
event_list_contains(events, kEventInProgressStartedLessThanOneHourAgoId));
EXPECT_FALSE(
event_list_contains(events, kEventInProgressStartedMoreThanOneHourAgoId));
EXPECT_FALSE(event_list_contains(events, kEventFinishedId));
}
} // namespace ash

@ -0,0 +1,113 @@
// 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.
#include "ash/system/time/calendar_up_next_view.h"
#include <memory>
#include "ash/bubble/bubble_utils.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/time/calendar_event_list_item_view_jelly.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_provider.h"
#include "ui/views/background.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/metadata/view_factory_internal.h"
namespace ash {
namespace {
constexpr int kContainerInsets = 12;
constexpr int kBackgroundRadius = 12;
constexpr int kBetweenChildSpacing = 8;
constexpr int kMaxEventListItemWidth = 160;
views::Builder<views::Label> CreateHeaderLabel() {
return views::Builder<views::Label>(bubble_utils::CreateLabel(
bubble_utils::TypographyStyle::kButton2,
l10n_util::GetStringUTF16(IDS_ASH_CALENDAR_UP_NEXT)));
}
} // namespace
CalendarUpNextView::CalendarUpNextView(
CalendarViewController* calendar_view_controller)
: calendar_view_controller_(calendar_view_controller),
header_view_(AddChildView(std::make_unique<views::View>())),
scroll_view_(AddChildView(std::make_unique<views::ScrollView>(
views::ScrollView::ScrollWithLayers::kEnabled))),
content_view_(
scroll_view_->SetContents(std::make_unique<views::View>())) {
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets(kContainerInsets),
kBetweenChildSpacing));
header_view_->SetLayoutManager(std::make_unique<views::FlexLayout>());
header_view_->AddChildView(CreateHeaderLabel().Build());
scroll_view_->SetAllowKeyboardScrolling(false);
scroll_view_->SetBackgroundColor(absl::nullopt);
scroll_view_->SetDrawOverflowIndicator(false);
scroll_view_->SetHorizontalScrollBarMode(
views::ScrollView::ScrollBarMode::kHiddenButEnabled);
scroll_view_->SetTreatAllScrollEventsAsHorizontal(true);
content_view_->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
kBetweenChildSpacing));
UpdateEvents();
}
CalendarUpNextView::~CalendarUpNextView() = default;
void CalendarUpNextView::Layout() {
// For some reason the `content_view_` is constrained to the `scroll_view_`
// width and so it isn't scrollable. This seems to be a problem with
// horizontal `ScrollView`s as this doesn't happen if you make this view
// vertically scrollable. To make the content scrollable, we need to set it's
// preferred size here so it's bigger than the `scroll_view_` and
// therefore scrolls.
if (content_view_)
content_view_->SizeToPreferredSize();
// `content_view_` is a child of this class so we need to Layout after
// changing its width.
views::View::Layout();
}
void CalendarUpNextView::OnThemeChanged() {
views::View::OnThemeChanged();
SetBackground(views::CreateRoundedRectBackground(
GetColorProvider()->GetColor(cros_tokens::kCrosSysSystemOnBase),
kBackgroundRadius));
}
void CalendarUpNextView::UpdateEvents() {
content_view_->RemoveAllChildViews();
std::list<google_apis::calendar::CalendarEvent> events =
calendar_view_controller_->UpcomingEvents();
auto now = base::Time::NowFromSystemTime();
for (auto& event : events) {
content_view_->AddChildView(
std::make_unique<CalendarEventListItemViewJelly>(
calendar_view_controller_,
SelectedDateParams{now, now.UTCMidnight(), now.LocalMidnight()},
/*event=*/event, /*round_top_corners=*/true,
/*round_bottom_corners=*/true,
/*max_width=*/kMaxEventListItemWidth));
}
}
BEGIN_METADATA(CalendarUpNextView, views::View);
END_METADATA
} // namespace ash

@ -0,0 +1,48 @@
// 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_SYSTEM_TIME_CALENDAR_UP_NEXT_VIEW_H_
#define ASH_SYSTEM_TIME_CALENDAR_UP_NEXT_VIEW_H_
#include "ash/ash_export.h"
#include "ash/system/time/calendar_view_controller.h"
#include "ui/views/view.h"
namespace ash {
// This view displays a scrollable list of `CalendarEventListItemView` for the
// events that a user has coming up, either imminently or that are already in
// progress but not yet finished.
class ASH_EXPORT CalendarUpNextView : public views::View {
public:
METADATA_HEADER(CalendarUpNextView);
explicit CalendarUpNextView(CalendarViewController* calendar_view_controller);
CalendarUpNextView(const CalendarUpNextView& other) = delete;
CalendarUpNextView& operator=(const CalendarUpNextView& other) = delete;
~CalendarUpNextView() override;
// views::View
void OnThemeChanged() override;
void Layout() override;
private:
friend class CalendarUpNextViewTest;
void UpdateEvents();
// Owned by `CalendarView`.
CalendarViewController* calendar_view_controller_;
// Owned by `CalendarUpNextView`.
views::View* const header_view_;
views::ScrollView* const scroll_view_;
// The content of the horizontal `scroll_view`, which carries a list of
// `CalendarEventListItemView`.
views::View* const content_view_;
};
} // namespace ash
#endif // ASH_SYSTEM_TIME_CALENDAR_UP_NEXT_VIEW_H_

@ -0,0 +1,129 @@
// 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.
#include "ash/system/time/calendar_up_next_view.h"
#include "ash/shell.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/time/calendar_unittest_utils.h"
#include "ash/system/time/calendar_view_controller.h"
#include "ash/test/ash_test_base.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "ui/views/controls/label.h"
namespace ash {
namespace {
std::unique_ptr<google_apis::calendar::CalendarEvent> CreateEvent(
const char* start_time,
const char* end_time,
bool all_day_event = false) {
return calendar_test_utils::CreateEvent(
"id_0", "summary_0", start_time, end_time,
google_apis::calendar::CalendarEvent::EventStatus::kConfirmed,
google_apis::calendar::CalendarEvent::ResponseStatus::kAccepted,
all_day_event);
}
std::unique_ptr<google_apis::calendar::EventList> CreateMockEventList(
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>>& events) {
auto event_list = std::make_unique<google_apis::calendar::EventList>();
event_list->set_time_zone("Greenwich Mean Time");
for (auto& event : events)
event_list->InjectItemForTesting(std::move(event));
return event_list;
}
} // namespace
class CalendarUpNextViewTest : public AshTestBase {
public:
CalendarUpNextViewTest() = default;
CalendarUpNextViewTest(const CalendarUpNextViewTest&) = delete;
CalendarUpNextViewTest& operator=(const CalendarUpNextViewTest&) = delete;
~CalendarUpNextViewTest() override = default;
void SetUp() override {
AshTestBase::SetUp();
controller_ = std::make_unique<CalendarViewController>();
}
void TearDown() override {
up_next_view_.reset();
controller_.reset();
AshTestBase::TearDown();
}
void CreateUpNextView(
base::Time date,
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>>&
events) {
up_next_view_.reset();
Shell::Get()->system_tray_model()->calendar_model()->OnEventsFetched(
calendar_utils::GetStartOfMonthUTC(date),
google_apis::ApiErrorCode::HTTP_SUCCESS,
CreateMockEventList(events).get());
up_next_view_ = std::make_unique<CalendarUpNextView>(controller_.get());
}
const views::Label* GetHeaderLabel() {
return static_cast<views::Label*>(
up_next_view_->header_view_->children()[0]);
}
const views::View* GetContentsView() {
return static_cast<views::View*>(up_next_view_->content_view_);
}
static base::Time FakeTimeNow() { return fake_time_; }
static void SetFakeNow(base::Time fake_now) { fake_time_ = fake_now; }
CalendarViewController* controller() { return controller_.get(); }
CalendarUpNextView* up_next_view() { return up_next_view_.get(); }
private:
std::unique_ptr<CalendarUpNextView> up_next_view_;
std::unique_ptr<CalendarViewController> controller_;
base::test::ScopedFeatureList features_;
static base::Time fake_time_;
};
base::Time CalendarUpNextViewTest::fake_time_;
TEST_F(CalendarUpNextViewTest,
GivenUpcomingEvents_WhenUpNextViewIsCreated_ThenShowEvents) {
base::Time date;
ASSERT_TRUE(base::Time::FromString("22 Nov 2021 09:00 GMT", &date));
// Set time override.
SetFakeNow(date);
base::subtle::ScopedTimeClockOverrides time_override(
&CalendarUpNextViewTest::FakeTimeNow, /*time_ticks_override=*/nullptr,
/*thread_ticks_override=*/nullptr);
// Event starts in 10 mins.
const char* event_in_ten_mins_start_time_string = "22 Nov 2021 09:10 GMT";
const char* event_in_ten_mins_end_time_string = "22 Nov 2021 10:00 GMT";
// Event in progress.
const char* event_in_progress_start_time_string = "22 Nov 2021 08:30 GMT";
const char* event_in_progress_end_time_string = "22 Nov 2021 09:30 GMT";
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
events.emplace_back(CreateEvent(event_in_ten_mins_start_time_string,
event_in_ten_mins_end_time_string));
events.emplace_back(CreateEvent(event_in_progress_start_time_string,
event_in_progress_end_time_string));
CreateUpNextView(date, events);
EXPECT_EQ(GetHeaderLabel()->GetText(), u"Up next");
EXPECT_EQ(GetContentsView()->children().size(), size_t(2));
}
} // namespace ash

@ -6,6 +6,7 @@
#include <memory>
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/ash_typography.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
@ -89,6 +90,9 @@ constexpr base::TimeDelta kAnimationDurationForClosingEvents =
// The cool-down time for enabling animation.
constexpr base::TimeDelta kAnimationDisablingTimeout = base::Milliseconds(500);
// Periodic time delay for checking upcoming events.
constexpr base::TimeDelta kCheckUpcomingEventsDelay = base::Seconds(15);
// The multiplier used to reduce velocity of flings on the calendar view.
// Without this, CalendarView will scroll a few years per fast swipe.
constexpr float kCalendarScrollFlingMultiplier = 0.25f;
@ -371,6 +375,8 @@ CalendarView::CalendarView(DetailedViewDelegate* delegate,
calendar_view->set_should_months_animate(true);
},
base::Unretained(this))) {
auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
SetFocusBehavior(FocusBehavior::ALWAYS);
// Focusable nodes must have an accessible name and valid role.
@ -443,6 +449,8 @@ CalendarView::CalendarView(DetailedViewDelegate* delegate,
// Add scroll view.
scroll_view_ = AddChildView(std::make_unique<views::ScrollView>());
// Flex the scrollview around any sibling views that are added or removed.
layout->SetFlexForView(scroll_view_, 1);
scroll_view_->SetAllowKeyboardScrolling(false);
scroll_view_->SetBackgroundColor(absl::nullopt);
scroll_view_->ClipHeightTo(0, INT_MAX);
@ -483,6 +491,11 @@ CalendarView::CalendarView(DetailedViewDelegate* delegate,
scoped_view_observer_.AddObservation(scroll_view_);
scoped_view_observer_.AddObservation(content_view_);
scoped_view_observer_.AddObservation(this);
check_upcoming_events_timer_.Start(
FROM_HERE, kCheckUpcomingEventsDelay,
base::BindRepeating(&CalendarView::MaybeShowUpNextView,
base::Unretained(this)));
}
CalendarView::~CalendarView() {
@ -497,6 +510,8 @@ CalendarView::~CalendarView() {
RemoveChildViewT(event_list_view_);
event_list_view_ = nullptr;
}
check_upcoming_events_timer_.Stop();
RemoveUpNextView();
content_view_->RemoveAllChildViews();
}
@ -712,17 +727,20 @@ void CalendarView::UpdateOnScreenMonthMap() {
MaybeUpdateLoadingBarVisibility();
}
void CalendarView::MaybeUpdateLoadingBarVisibility() {
bool CalendarView::EventsFetchComplete() {
for (auto& it : on_screen_month_) {
// If there's an on-screen month that hasn't finished fetching or
// re-fetching, the loading bar should be visible.
// Return false if there's an on-screen month that hasn't finished fetching
// or re-fetching.
if (it.second == CalendarModel::kFetching ||
it.second == CalendarModel::kRefetching) {
ShowProgress(-1, true);
return;
return false;
}
}
ShowProgress(-1, false);
return true;
}
void CalendarView::MaybeUpdateLoadingBarVisibility() {
ShowProgress(-1, !EventsFetchComplete());
}
void CalendarView::FadeInCurrentMonth() {
@ -995,6 +1013,11 @@ void CalendarView::OnEventsFetched(
on_screen_month_[start_time] = status;
MaybeUpdateLoadingBarVisibility();
// Only show up next for events that are the same month as `base::Time::Now`.
if (start_time == calendar_utils::GetStartOfMonthUTC(
base::Time::NowFromSystemTime().UTCMidnight()))
MaybeShowUpNextView();
}
void CalendarView::OnTimeout(const base::Time start_time) {
@ -1799,6 +1822,30 @@ void CalendarView::SetEventListViewBounds() {
kEventListViewVerticalPadding);
}
void CalendarView::MaybeShowUpNextView() {
if (features::IsCalendarJellyEnabled() && EventsFetchComplete() &&
!calendar_view_controller_->UpcomingEvents().empty()) {
if (up_next_view_)
return;
up_next_view_ = AddChildView(
std::make_unique<CalendarUpNextView>(calendar_view_controller_.get()));
} else {
RemoveUpNextView();
}
}
void CalendarView::RemoveUpNextView() {
if (up_next_view_) {
RemoveChildViewT(up_next_view_);
up_next_view_ = nullptr;
// If the up next view is deleted whilst the calendar is still open, e.g.
// time has passed and an event no longer meets 'upcoming' criteria, then
// the calendar view needs to relayout after removing the upnext view.
InvalidateLayout();
}
}
BEGIN_METADATA(CalendarView, views::View)
END_METADATA

@ -12,6 +12,7 @@
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/time/calendar_model.h"
#include "ash/system/time/calendar_up_next_view.h"
#include "ash/system/time/calendar_view_controller.h"
#include "ash/system/tray/tray_detailed_view.h"
#include "ash/system/unified/unified_system_tray_controller.h"
@ -231,6 +232,9 @@ class ASH_EXPORT CalendarView : public CalendarModel::Observer,
// Updates the on-screen month map with the current months on screen.
void UpdateOnScreenMonthMap();
// Returns whether or not we've finished fetching CalendarEvents.
bool EventsFetchComplete();
// Checks if all months in the visible window have finished fetching. If so,
// stop showing the loading bar.
void MaybeUpdateLoadingBarVisibility();
@ -324,6 +328,12 @@ class ASH_EXPORT CalendarView : public CalendarModel::Observer,
// bounds.
void SetEventListViewBounds();
// Conditionally displays the "Up next" view.
void MaybeShowUpNextView();
// Removes the "Up next" view.
void RemoveUpNextView();
// Setters for animation flags.
void set_should_header_animate(bool should_animate) {
should_header_animate_ = should_animate;
@ -362,6 +372,7 @@ class ASH_EXPORT CalendarView : public CalendarModel::Observer,
IconButton* up_button_ = nullptr;
IconButton* down_button_ = nullptr;
CalendarEventListView* event_list_view_ = nullptr;
CalendarUpNextView* up_next_view_ = nullptr;
std::map<base::Time, CalendarModel::FetchingStatus> on_screen_month_;
CalendarModel* calendar_model_ =
Shell::Get()->system_tray_model()->calendar_model();
@ -399,6 +410,9 @@ class ASH_EXPORT CalendarView : public CalendarModel::Observer,
base::RetainingOneShotTimer header_animation_restart_timer_;
base::RetainingOneShotTimer months_animation_restart_timer_;
// Timer that checks upcoming events periodically.
base::RepeatingTimer check_upcoming_events_timer_;
base::CallbackListSubscription on_contents_scrolled_subscription_;
base::ScopedObservation<CalendarModel, CalendarModel::Observer>
scoped_calendar_model_observer_{this};

@ -158,6 +158,14 @@ CalendarViewController::SelectedDateEventsSplitByMultiDayAndSameDay() {
ApplyTimeDifference(selected_date_.value()));
}
SingleDayEventList CalendarViewController::UpcomingEvents() {
return Shell::Get()
->system_tray_model()
->calendar_model()
->FindUpcomingEvents(
ApplyTimeDifference(base::Time::NowFromSystemTime()));
}
int CalendarViewController::GetEventNumber(base::Time date) {
return Shell::Get()->system_tray_model()->calendar_model()->EventsNumberOfDay(
ApplyTimeDifference(date),

@ -116,6 +116,9 @@ class ASH_EXPORT CalendarViewController {
std::tuple<SingleDayEventList, SingleDayEventList>
SelectedDateEventsSplitByMultiDayAndSameDay();
// Returns upcoming events for the "Up next" view.
SingleDayEventList UpcomingEvents();
// The calendar events number of the `date`.
int GetEventNumber(base::Time date);

@ -189,6 +189,8 @@ class CalendarViewTest : public AshTestBase {
}
views::View* event_list_view() { return calendar_view_->event_list_view_; }
views::View* up_next_view() { return calendar_view_->up_next_view_; }
void ScrollUpOneMonth() {
calendar_view_->ScrollOneMonthAndAutoScroll(/*scroll_up=*/true);
}
@ -2142,4 +2144,144 @@ TEST_F(CalendarViewWithMessageCenterTest,
EXPECT_EQ(current_date_cell_view, calendar_focus_manager()->GetFocusedView());
}
class CalendarViewWithJellyEnabledTest : public CalendarViewTest {
public:
CalendarViewWithJellyEnabledTest() = default;
CalendarViewWithJellyEnabledTest(const CalendarViewWithJellyEnabledTest&) =
delete;
CalendarViewWithJellyEnabledTest& operator=(
const CalendarViewWithJellyEnabledTest&) = delete;
~CalendarViewWithJellyEnabledTest() override = default;
void SetUp() override {
scoped_feature_list_ = std::make_unique<base::test::ScopedFeatureList>();
scoped_feature_list_->InitWithFeatures(
{features::kCalendarView, features::kCalendarJelly}, {});
CalendarViewTest::SetUp();
}
// Assumes current time is "18 Nov 2021 10:00 GMT".
std::unique_ptr<google_apis::calendar::EventList>
CreateMockEventListWithEventStartTimeMoreThanTwoHoursAway() {
auto event_list = std::make_unique<google_apis::calendar::EventList>();
event_list->set_time_zone("Greenwich Mean Time");
event_list->InjectItemForTesting(calendar_test_utils::CreateEvent(
"id_0", "summary_0", "18 Nov 2021 12:30 GMT", "18 Nov 2021 13:30 GMT"));
return event_list;
}
// Assumes current time is "18 Nov 2021 10:00 GMT".
std::unique_ptr<google_apis::calendar::EventList>
CreateMockEventListWithEventStartTimeTenMinsAway() {
auto event_list = std::make_unique<google_apis::calendar::EventList>();
event_list->set_time_zone("Greenwich Mean Time");
event_list->InjectItemForTesting(calendar_test_utils::CreateEvent(
"id_0", "summary_0", "18 Nov 2021 10:10 GMT", "18 Nov 2021 13:30 GMT"));
return event_list;
}
void MockEventsFetched(
base::Time date,
std::unique_ptr<google_apis::calendar::EventList> event_list) {
Shell::Get()->system_tray_model()->calendar_model()->OnEventsFetched(
calendar_utils::GetStartOfMonthUTC(date),
google_apis::ApiErrorCode::HTTP_SUCCESS, event_list.get());
}
private:
std::unique_ptr<base::test::ScopedFeatureList> scoped_feature_list_;
};
TEST_F(CalendarViewWithJellyEnabledTest,
GivenNoEvents_WhenCalendarViewOpens_ThenUpNextViewShouldNotBeShown) {
base::Time date;
ASSERT_TRUE(base::Time::FromString("7 Jun 2021 10:00 GMT", &date));
// Set time override.
SetFakeNow(date);
base::subtle::ScopedTimeClockOverrides time_override(
&CalendarViewTest::FakeTimeNow, /*time_ticks_override=*/nullptr,
/*thread_ticks_override=*/nullptr);
Shell::Get()->session_controller()->GetActivePrefService()->SetBoolean(
ash::prefs::kCalendarIntegrationEnabled, false);
CreateCalendarView();
// When we've just created the calendar view and not fetched any events, then
// up next shouldn't have been created.
bool is_showing_up_next_view = up_next_view();
EXPECT_FALSE(is_showing_up_next_view);
}
TEST_F(
CalendarViewWithJellyEnabledTest,
GivenEventsStartingMoreThanTwoHoursAway_WhenCalendarViewOpens_ThenUpNextViewShouldNotBeShown) {
base::Time date;
ASSERT_TRUE(base::Time::FromString("18 Nov 2021 10:00 GMT", &date));
// Set time override.
SetFakeNow(date);
base::subtle::ScopedTimeClockOverrides time_override(
&CalendarViewTest::FakeTimeNow, /*time_ticks_override=*/nullptr,
/*thread_ticks_override=*/nullptr);
CreateCalendarView();
MockEventsFetched(
calendar_utils::GetStartOfMonthUTC(date),
CreateMockEventListWithEventStartTimeMoreThanTwoHoursAway());
// When fetched events are more than two hours away, then up next shouldn't
// have been created.
bool is_showing_up_next_view = up_next_view();
EXPECT_FALSE(is_showing_up_next_view);
}
TEST_F(
CalendarViewWithJellyEnabledTest,
GivenEventsStartingTenMinsAway_WhenCalendarViewOpens_ThenUpNextViewShouldBeShown) {
base::Time date;
ASSERT_TRUE(base::Time::FromString("18 Nov 2021 10:00 GMT", &date));
// Set time override.
SetFakeNow(date);
base::subtle::ScopedTimeClockOverrides time_override(
&CalendarViewTest::FakeTimeNow, /*time_ticks_override=*/nullptr,
/*thread_ticks_override=*/nullptr);
CreateCalendarView();
MockEventsFetched(calendar_utils::GetStartOfMonthUTC(date),
CreateMockEventListWithEventStartTimeTenMinsAway());
// When fetched events are in the next 10 mins, then up next should have been
// created.
bool is_showing_up_next_view = up_next_view();
EXPECT_TRUE(is_showing_up_next_view);
}
TEST_F(
CalendarViewWithJellyEnabledTest,
GivenUpNextIsShown_WhenNewEventsMoreThanTwoHoursAwayAreFetched_ThenUpNextViewShouldNotBeShown) {
base::Time date;
ASSERT_TRUE(base::Time::FromString("18 Nov 2021 10:00 GMT", &date));
// Set time override.
SetFakeNow(date);
base::subtle::ScopedTimeClockOverrides time_override(
&CalendarViewTest::FakeTimeNow, /*time_ticks_override=*/nullptr,
/*thread_ticks_override=*/nullptr);
CreateCalendarView();
MockEventsFetched(calendar_utils::GetStartOfMonthUTC(date),
CreateMockEventListWithEventStartTimeTenMinsAway());
// When fetched events are in the next 10 mins, then up next should have been
// created.
EXPECT_TRUE(up_next_view());
MockEventsFetched(
calendar_utils::GetStartOfMonthUTC(date),
CreateMockEventListWithEventStartTimeMoreThanTwoHoursAway());
// When fetched events are now more than two hours away, then up next
// should have been destroyed.
EXPECT_FALSE(up_next_view());
}
} // namespace ash