0

Add "Join" meeting button to calendar events.

This CL adds a "Join" meeting button that will launch Google Meet, if one is linked to the meeting. If there is no Google Meet meeting linked, then the button will not be added to the view hierarchy.

If the Google Meet app is installed, it will launch in that, otherwise it will launch in the browser.

Screenshot: http://shortn/_B8OBKpTU07

Bug: b/266537227
Change-Id: I459e3a2e9214032ef51d18bbc46f0312f4ee0401
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4224684
Reviewed-by: Jiaming Cheng <jiamingc@chromium.org>
Reviewed-by: Yulun Wu <yulunwu@chromium.org>
Reviewed-by: Ahmed Fakhry <afakhry@chromium.org>
Commit-Queue: Sam Cackett <samcackett@google.com>
Reviewed-by: David Roger <droger@chromium.org>
Reviewed-by: Jimmy Gong <jimmyxgong@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1107380}
This commit is contained in:
Sam Cackett
2023-02-20 10:34:46 +00:00
committed by Chromium LUCI CQ
parent 001bf99d8f
commit 18016baecc
28 changed files with 432 additions and 202 deletions

@ -4725,6 +4725,14 @@ Connect your device to power.
See all events for today
</message>
<message name="IDS_ASH_CALENDAR_JOIN_BUTTON" desc="The join meeting button on the event list item views.">
Join
</message>
<message name="IDS_ASH_CALENDAR_JOIN_BUTTON_ACCESSIBLE_NAME" desc="The join meeting button accessible name on the event list item views.">
Hit enter to join event <ph name="event_summary">$1<ex>Lunch break</ex></ph>
</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 @@
d303d9dd34263049634f5bf4bdaf7fe9e088db79

@ -0,0 +1 @@
a897cbbf687238c380b5b0b3b641cc7c194fb4c8

@ -162,6 +162,10 @@ class ASH_PUBLIC_EXPORT SystemTrayClient {
bool& opened_pwa,
GURL& finalized_event_url) = 0;
// Launches Google Meet from the given URL.
// Opens in the Google Meet PWA if installed, otherwise opens in the browser.
virtual void ShowGoogleMeet(const std::string& hangout_link) = 0;
// Shown when the device is on a non-stable release track and the user clicks
// the channel/version button from quick settings.
virtual void ShowChannelInfoAdditionalDetails() = 0;

@ -134,6 +134,10 @@ void TestSystemTrayClient::ShowCalendarEvent(
show_calendar_event_count_++;
}
void TestSystemTrayClient::ShowGoogleMeet(const std::string& hangout_link) {
show_google_meet_count_++;
}
void TestSystemTrayClient::ShowChannelInfoAdditionalDetails() {
++show_channel_info_additional_details_count_;
}

@ -67,6 +67,7 @@ class ASH_PUBLIC_EXPORT TestSystemTrayClient : public SystemTrayClient {
const base::Time& date,
bool& opened_pwa,
GURL& final_event_url) override;
void ShowGoogleMeet(const std::string& hangout_link) override;
void ShowChannelInfoAdditionalDetails() override;
void ShowChannelInfoGiveFeedback() override;
void ShowAudioSettings() override;
@ -134,6 +135,8 @@ class ASH_PUBLIC_EXPORT TestSystemTrayClient : public SystemTrayClient {
int show_calendar_event_count() const { return show_calendar_event_count_; }
int show_google_meet_count() const { return show_google_meet_count_; }
const std::string& last_network_type() const { return last_network_type_; }
int show_firmware_update_count() const { return show_firmware_update_count_; }
@ -179,6 +182,7 @@ class ASH_PUBLIC_EXPORT TestSystemTrayClient : public SystemTrayClient {
int show_network_create_count_ = 0;
int show_access_code_casting_dialog_count_ = 0;
int show_calendar_event_count_ = 0;
int show_google_meet_count_ = 0;
std::string last_bluetooth_settings_device_id_;
std::string last_network_settings_network_id_;
std::string last_network_type_;

@ -12,6 +12,7 @@
#include "ash/public/cpp/system_tray_client.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/pill_button.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/time/calendar_metrics.h"
#include "ash/system/time/calendar_utils.h"
@ -141,7 +142,8 @@ CalendarEventListItemViewJelly::CalendarEventListItemViewJelly(
: ActionableView(TrayPopupInkDropStyle::FILL_BOUNDS),
calendar_view_controller_(calendar_view_controller),
selected_date_params_(selected_date_params),
event_url_(event.html_link()) {
event_url_(event.html_link()),
hangout_link_(event.hangout_link()) {
SetLayoutManager(std::make_unique<views::FillLayout>());
const auto [start_time, end_time] = calendar_utils::GetStartAndEndTime(
@ -186,17 +188,28 @@ CalendarEventListItemViewJelly::CalendarEventListItemViewJelly(
auto horizontal_layout_manager = std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal, kEventListItemInsets,
kEventListItemHorizontalChildSpacing);
horizontal_layout_manager->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kStart);
views::View* horizontal_container =
AddChildView(std::make_unique<views::View>());
horizontal_container->SetLayoutManager(std::move(horizontal_layout_manager));
auto* horizontal_container_layout_manager =
horizontal_container->SetLayoutManager(
std::move(horizontal_layout_manager));
// Event list dot.
if (show_event_list_dot) {
horizontal_container
views::View* event_list_dot_container =
horizontal_container->AddChildView(std::make_unique<views::View>());
auto* layout_vertical_start = event_list_dot_container->SetLayoutManager(
std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
layout_vertical_start->set_main_axis_alignment(
views::BoxLayout::MainAxisAlignment::kStart);
event_list_dot_container
->AddChildView(
std::make_unique<CalendarEventListItemDot>(event.color_id()))
->SetID(kEventListItemDotID);
}
// Labels.
views::View* vertical_container =
horizontal_container->AddChildView(std::make_unique<views::View>());
vertical_container->SetLayoutManager(std::make_unique<views::BoxLayout>(
@ -205,6 +218,29 @@ CalendarEventListItemViewJelly::CalendarEventListItemViewJelly(
CreateSummaryLabel(event.summary(), tooltip_text, fixed_width).Build());
vertical_container->AddChildView(
CreateTimeLabel(formatted_time_text, tooltip_text).Build());
horizontal_container_layout_manager->SetFlexForView(vertical_container, 1);
// Join button.
if (!event.hangout_link().empty()) {
views::View* join_button_container =
horizontal_container->AddChildView(std::make_unique<views::View>());
auto* layout_vertical_center = join_button_container->SetLayoutManager(
std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
layout_vertical_center->set_main_axis_alignment(
views::BoxLayout::MainAxisAlignment::kCenter);
auto join_button = std::make_unique<PillButton>(
base::BindRepeating(
&CalendarEventListItemViewJelly::OnJoinMeetingButtonPressed,
weak_ptr_factory_.GetWeakPtr()),
l10n_util::GetStringUTF16(IDS_ASH_CALENDAR_JOIN_BUTTON),
PillButton::Type::kPrimaryWithoutIcon);
join_button->SetAccessibleName(
l10n_util::GetStringFUTF16(IDS_ASH_CALENDAR_JOIN_BUTTON_ACCESSIBLE_NAME,
base::UTF8ToUTF16(event.summary())));
join_button->SetID(kJoinButtonID);
join_button_container->AddChildView(std::move(join_button));
}
}
CalendarEventListItemViewJelly::~CalendarEventListItemViewJelly() = default;
@ -229,6 +265,13 @@ bool CalendarEventListItemViewJelly::PerformAction(const ui::Event& event) {
return true;
}
void CalendarEventListItemViewJelly::OnJoinMeetingButtonPressed(
const ui::Event& event) {
calendar_view_controller_->RecordJoinMeetingButtonPressed(event);
Shell::Get()->system_tray_model()->client()->ShowGoogleMeet(hangout_link_);
}
BEGIN_METADATA(CalendarEventListItemViewJelly, views::View);
END_METADATA

@ -21,6 +21,7 @@ namespace ash {
constexpr int kSummaryLabelID = 100;
constexpr int kTimeLabelID = 101;
constexpr int kEventListItemDotID = 102;
constexpr int kJoinButtonID = 103;
class CalendarViewController;
@ -59,6 +60,8 @@ class ASH_EXPORT CalendarEventListItemViewJelly : public ActionableView {
// ActionableView:
bool PerformAction(const ui::Event& event) override;
void OnJoinMeetingButtonPressed(const ui::Event& event);
private:
friend class CalendarViewEventListViewTest;
@ -69,6 +72,10 @@ class ASH_EXPORT CalendarEventListItemViewJelly : public ActionableView {
// The URL for the meeting event.
const GURL event_url_;
const std::string hangout_link_;
base::WeakPtrFactory<CalendarEventListItemViewJelly> weak_ptr_factory_{this};
};
} // namespace ash

@ -20,12 +20,13 @@ namespace {
std::unique_ptr<google_apis::calendar::CalendarEvent> CreateEvent(
const char* start_time,
const char* end_time,
bool all_day_event = false) {
bool all_day_event = false,
std::string hangout_link = "") {
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);
all_day_event, hangout_link);
}
} // namespace
@ -91,6 +92,11 @@ class CalendarViewEventListItemViewJellyTest : public AshTestBase {
event_list_item_view_jelly_->GetViewByID(kEventListItemDotID));
}
const views::Button* GetJoinButton() {
return static_cast<views::Button*>(
event_list_item_view_jelly_->GetViewByID(kJoinButtonID));
}
CalendarViewController* controller() { return controller_.get(); }
CalendarEventListItemViewJelly* event_list_item_view() {
@ -234,4 +240,37 @@ TEST_F(CalendarViewEventListItemViewJellyTest,
EXPECT_TRUE(GetEventListItemDot());
}
TEST_F(CalendarViewEventListItemViewJellyTest,
ShouldShowJoinMeetingButton_WhenGoogleMeetLinkExists) {
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, false,
"https://meet.google.com/my-meeting");
CreateEventListItemView(date, event.get(), /*round_top_corners=*/true,
/*round_bottom_corners=*/true,
/*show_event_list_dot=*/false);
EXPECT_TRUE(GetJoinButton());
}
TEST_F(CalendarViewEventListItemViewJellyTest,
ShouldHideJoinMeetingButton_WhenGoogleMeetLinkDoesNotExist) {
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);
CreateEventListItemView(date, event.get(), /*round_top_corners=*/true,
/*round_bottom_corners=*/true,
/*show_event_list_dot=*/false);
EXPECT_FALSE(GetJoinButton());
}
} // namespace ash

@ -37,6 +37,10 @@ constexpr char kCalendarEventListItemInUpNextPressed[] =
"Ash.Calendar.UpNextView.EventListItem.Pressed";
constexpr char kCalendarUpNextEventDisplayedCount[] =
"Ash.Calendar.UpNextView.EventDisplayedCount";
constexpr char kCalendarEventListItemJoinButtonPressed[] =
"Ash.Calendar.EventListView.JoinMeetingButton.Pressed";
constexpr char kCalendarUpNextJoinButtonPressed[] =
"Ash.Calendar.UpNextView.JoinMeetingButton.Pressed";
} // namespace
@ -127,6 +131,16 @@ void RecordUpNextEventCount(const int event_count) {
base::UmaHistogramCounts100(kCalendarUpNextEventDisplayedCount, event_count);
}
void RecordJoinButtonPressedFromEventListView(const ui::Event& event) {
base::UmaHistogramEnumeration(kCalendarEventListItemJoinButtonPressed,
GetEventType(event));
}
void RecordJoinButtonPressedFromUpNextView(const ui::Event& event) {
base::UmaHistogramEnumeration(kCalendarUpNextJoinButtonPressed,
GetEventType(event));
}
} // namespace calendar_metrics
} // namespace ash

@ -100,6 +100,10 @@ void RecordEventListItemInUpNextLaunched(const ui::Event& event);
void RecordUpNextEventCount(const int event_count);
void RecordJoinButtonPressedFromEventListView(const ui::Event& event);
void RecordJoinButtonPressedFromUpNextView(const ui::Event& event);
} // namespace calendar_metrics
} // namespace ash

@ -46,7 +46,8 @@ std::unique_ptr<google_apis::calendar::CalendarEvent> CreateEvent(
const google_apis::calendar::CalendarEvent::EventStatus event_status,
const google_apis::calendar::CalendarEvent::ResponseStatus
self_response_status,
const bool all_day_event) {
const bool all_day_event,
const std::string hangout_link) {
auto event = std::make_unique<google_apis::calendar::CalendarEvent>();
base::Time start_time_base, end_time_base;
google_apis::calendar::DateTime start_time_date, end_time_date;
@ -70,6 +71,7 @@ std::unique_ptr<google_apis::calendar::CalendarEvent> CreateEvent(
event->set_status(event_status);
event->set_self_response_status(self_response_status);
event->set_all_day_event(all_day_event);
event->set_hangout_link(hangout_link);
return event;
}
@ -81,7 +83,8 @@ std::unique_ptr<google_apis::calendar::CalendarEvent> CreateEvent(
const google_apis::calendar::CalendarEvent::EventStatus event_status,
const google_apis::calendar::CalendarEvent::ResponseStatus
self_response_status,
const bool all_day_event) {
const bool all_day_event,
const std::string hangout_link) {
auto event = std::make_unique<google_apis::calendar::CalendarEvent>();
google_apis::calendar::DateTime start_time_date, end_time_date;
event->set_id(id);
@ -93,6 +96,7 @@ std::unique_ptr<google_apis::calendar::CalendarEvent> CreateEvent(
event->set_status(event_status);
event->set_self_response_status(self_response_status);
event->set_all_day_event(all_day_event);
event->set_hangout_link(hangout_link);
return event;
}

@ -257,7 +257,8 @@ std::unique_ptr<google_apis::calendar::CalendarEvent> CreateEvent(
const google_apis::calendar::CalendarEvent::ResponseStatus
self_response_status =
google_apis::calendar::CalendarEvent::ResponseStatus::kAccepted,
const bool all_day_event = false);
const bool all_day_event = false,
const std::string hangout_link = "");
// Creates a `google_apis::calendar::CalendarEvent` for testing, that converts
// start/end `base::Time` objects to `google_apis::calendar::DateTime`.
@ -271,7 +272,8 @@ std::unique_ptr<google_apis::calendar::CalendarEvent> CreateEvent(
const google_apis::calendar::CalendarEvent::ResponseStatus
self_response_status =
google_apis::calendar::CalendarEvent::ResponseStatus::kAccepted,
const bool all_day_event = false);
const bool all_day_event = false,
const std::string hangout_link = "");
std::unique_ptr<google_apis::calendar::EventList> CreateMockEventList(
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events);

@ -23,12 +23,13 @@ std::unique_ptr<google_apis::calendar::CalendarEvent> CreateEvent(
const base::Time start_time,
const base::Time end_time,
const char* summary = "Event with long name that should ellipsis",
bool all_day_event = false) {
bool all_day_event = false,
const std::string hangout_link = "") {
return calendar_test_utils::CreateEvent(
"id_0", summary, start_time, end_time,
google_apis::calendar::CalendarEvent::EventStatus::kConfirmed,
google_apis::calendar::CalendarEvent::ResponseStatus::kAccepted,
all_day_event);
all_day_event, hangout_link);
}
} // namespace
@ -185,4 +186,26 @@ TEST_F(
/*revision_number=*/0, Widget()));
}
TEST_F(CalendarUpNextViewPixelTest, ShouldShowJoinMeetingButton) {
// Set time override.
base::subtle::ScopedTimeClockOverrides time_override(
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add an upcoming event with a hangout_link.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto start_time = base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
events.push_back(CreateEvent(start_time, end_time, "First event", false,
"https://meet.google.com/abc-123"));
CreateCalendarUpNextView(std::move(events));
EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
"calendar_up_next_join_button",
/*revision_number=*/0, Widget()));
}
} // namespace ash

@ -45,16 +45,12 @@ constexpr int kCombinedViewMargin =
kCalendarUpNextViewStartEndMargin * 2 +
calendar_utils::kEventListItemViewStartEndMargin * 2;
// At full width (displaying a single event) the label should be the tray width
// minus `kCombinedViewMargin`, otherwise it'll become scrollable.
// TODO(b/266537227): When adding the join button, also reduce this off the
// size.
constexpr int kLabelFullWidth = kTrayMenuWidth - kCombinedViewMargin;
// and so have no max applied.
constexpr int kLabelFullWidth = 0;
// UI spec is a fixed 240 width for the whole up next event list item view, if
// there's more than 1 being shown. Given we're achieving this using
// `SizeToFit()` on a label, the label will need to account for the
// `kCombinedViewMargin` so we reduce those off the size.
// TODO (b/266537227): When adding the join button, also reduce this off the
// size.
constexpr int kLabelCappedWidth = 240 - kCombinedViewMargin;
constexpr gfx::Insets kHeaderInsets = gfx::Insets::TLBR(0, 0, 6, 0);
constexpr int kHeaderBetweenChildSpacing = 14;

@ -4,8 +4,10 @@
#include "ash/system/time/calendar_up_next_view.h"
#include "ash/public/cpp/test/test_system_tray_client.h"
#include "ash/shell.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/time/calendar_event_list_item_view_jelly.h"
#include "ash/system/time/calendar_unittest_utils.h"
#include "ash/system/time/calendar_view_controller.h"
#include "ash/system/tray/tray_constants.h"
@ -23,12 +25,32 @@ namespace {
std::unique_ptr<google_apis::calendar::CalendarEvent> CreateEvent(
const base::Time start_time,
const base::Time end_time,
bool all_day_event = false) {
bool all_day_event = false,
std::string hangout_link = "") {
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);
all_day_event, hangout_link);
}
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>>
CreateUpcomingEvents(int event_count = 1,
bool all_day_event = false,
std::string hangout_link = "") {
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
for (int i = 0; i < event_count; ++i) {
events.push_back(CreateEvent(event_in_ten_mins_start_time,
event_in_ten_mins_end_time, all_day_event,
hangout_link));
}
return events;
}
} // namespace
@ -174,17 +196,8 @@ TEST_F(CalendarUpNextViewTest,
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add single event starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
CreateUpNextView(std::move(events));
// Create UpNextView with a single upcoming event.
CreateUpNextView(CreateUpcomingEvents());
EXPECT_EQ(GetContentsView()->children().size(), size_t(1));
EXPECT_EQ(GetContentsView()->children()[0]->width(),
@ -198,21 +211,11 @@ TEST_F(CalendarUpNextViewTest,
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add multiple events starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
for (int i = 0; i < 5; ++i) {
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
}
// Add multiple upcoming events.
const int event_count = 5;
CreateUpNextView(CreateUpcomingEvents(event_count));
CreateUpNextView(std::move(events));
EXPECT_EQ(GetContentsView()->children().size(), size_t(5));
EXPECT_EQ(GetContentsView()->children().size(), size_t(event_count));
EXPECT_EQ(ScrollPosition(), 0);
// Press scroll right. We should scroll past the first event + margin.
@ -247,17 +250,8 @@ TEST_F(CalendarUpNextViewTest, ShouldHideScrollButtons_WhenOnlyOneEvent) {
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add single event starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
CreateUpNextView(std::move(events));
// Create UpNextView with a single upcoming event.
CreateUpNextView(CreateUpcomingEvents());
EXPECT_EQ(GetContentsView()->children().size(), size_t(1));
EXPECT_EQ(ScrollPosition(), 0);
@ -274,21 +268,10 @@ TEST_F(CalendarUpNextViewTest, ShouldShowScrollButtons_WhenMultipleEvents) {
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add multiple events starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
for (int i = 0; i < 5; ++i) {
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
}
CreateUpNextView(std::move(events));
EXPECT_EQ(GetContentsView()->children().size(), size_t(5));
// Add multiple upcoming events.
const int event_count = 5;
CreateUpNextView(CreateUpcomingEvents(event_count));
EXPECT_EQ(GetContentsView()->children().size(), size_t(event_count));
// At the start the scroll left button should be disabled and visible.
EXPECT_EQ(ScrollPosition(), 0);
@ -334,21 +317,10 @@ TEST_F(
calendar_test_utils::ScopedLibcTimeZone scoped_libc_timezone("GMT");
ASSERT_TRUE(scoped_libc_timezone.is_success());
// Add multiple events starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
for (int i = 0; i < 5; ++i) {
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
}
CreateUpNextView(std::move(events));
EXPECT_EQ(GetContentsView()->children().size(), size_t(5));
// Add multiple upcoming events.
const int event_count = 5;
CreateUpNextView(CreateUpcomingEvents(event_count));
EXPECT_EQ(GetContentsView()->children().size(), size_t(event_count));
EXPECT_EQ(ScrollPosition(), 0);
// Scroll right past the first event and so that the second event is partially
@ -391,21 +363,10 @@ TEST_F(
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add multiple events starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
for (int i = 0; i < 5; ++i) {
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
}
CreateUpNextView(std::move(events));
EXPECT_EQ(GetContentsView()->children().size(), size_t(5));
// Add multiple upcoming events.
const int event_count = 5;
CreateUpNextView(CreateUpcomingEvents(event_count));
EXPECT_EQ(GetContentsView()->children().size(), size_t(event_count));
EXPECT_EQ(ScrollPosition(), 0);
ScrollHorizontalPositionTo(100);
@ -430,21 +391,12 @@ TEST_F(CalendarUpNextViewTest,
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add event starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
bool called = false;
auto callback = base::BindLambdaForTesting(
[&called](const ui::Event& event) { called = true; });
CreateUpNextView(std::move(events), callback);
// Create UpNextView with a single upcoming event.
CreateUpNextView(CreateUpcomingEvents(), callback);
EXPECT_FALSE(called);
LeftClickOn(GetTodaysEventsButton());
@ -458,19 +410,9 @@ TEST_F(CalendarUpNextViewTest, ShouldTrackLaunchingFromEventListItem) {
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add an upcoming event.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
// Create UpNextView with a single upcoming event.
auto histogram_tester = std::make_unique<base::HistogramTester>();
CreateUpNextView(std::move(events));
CreateUpNextView(CreateUpcomingEvents());
EXPECT_EQ(GetContentsView()->children().size(), size_t(1));
// Click event inside the scrollview contents.
@ -487,27 +429,40 @@ TEST_F(CalendarUpNextViewTest, ShouldTrackEventDisplayedCount) {
nullptr, nullptr);
// Add 5 upcoming events.
const int event_count = 5;
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
for (int i = 0; i < event_count; ++i) {
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
}
auto histogram_tester = std::make_unique<base::HistogramTester>();
CreateUpNextView(std::move(events));
const int event_count = 5;
CreateUpNextView(CreateUpcomingEvents(event_count));
EXPECT_EQ(GetContentsView()->children().size(), size_t(event_count));
histogram_tester->ExpectBucketCount(
"Ash.Calendar.UpNextView.EventDisplayedCount", event_count, 1);
}
TEST_F(CalendarUpNextViewTest,
ShouldLaunchAndTrackGoogleMeet_WhenJoinMeetingButtonPressed) {
// Set time override.
base::subtle::ScopedTimeClockOverrides time_override(
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
auto histogram_tester = std::make_unique<base::HistogramTester>();
// Create up next view with upcoming google meet event.
CreateUpNextView(
CreateUpcomingEvents(1, false, "https://meet.google.com/abc-123"));
EXPECT_EQ(GetContentsView()->children().size(), size_t(1));
EXPECT_EQ(GetSystemTrayClient()->show_google_meet_count(), 0);
// Click the "Join" meeting button.
const auto* join_meeting_button =
GetContentsView()->children()[0]->GetViewByID(kJoinButtonID);
ASSERT_TRUE(join_meeting_button);
LeftClickOn(join_meeting_button);
EXPECT_EQ(GetSystemTrayClient()->show_google_meet_count(), 1);
histogram_tester->ExpectTotalCount(
"Ash.Calendar.UpNextView.JoinMeetingButton.Pressed", 1);
}
class CalendarUpNextViewAnimationTest : public CalendarUpNextViewTest {
public:
CalendarUpNextViewAnimationTest()
@ -532,16 +487,9 @@ class CalendarUpNextViewAnimationTest : public CalendarUpNextViewTest {
// Flaky: https://crbug.com/1401505
TEST_F(CalendarUpNextViewAnimationTest,
DISABLED_ShouldAnimateScrollView_WhenScrollButtonsArePressed) {
// Add multiple events starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time = base::Time::Now() + base::Minutes(10);
auto event_in_ten_mins_end_time = base::Time::Now() + base::Hours(1);
for (int i = 0; i < 5; ++i) {
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
}
CreateUpNextView(std::move(events));
// Add multiple upcoming events.
const int event_count = 5;
CreateUpNextView(CreateUpcomingEvents(event_count));
EXPECT_FALSE(IsAnimating());
PressScrollRightButton();

@ -227,6 +227,19 @@ void CalendarViewController::RecordEventListItemActivated(
calendar_metrics::RecordEventListItemInUpNextLaunched(event);
}
void CalendarViewController::RecordJoinMeetingButtonPressed(
const ui::Event& event) {
// The EventListItemView is used by both the event list view and the up next
// view. So if the event list view is not showing, then it's in the up next
// view.
if (is_event_list_showing_) {
calendar_metrics::RecordJoinButtonPressedFromEventListView(event);
return;
}
calendar_metrics::RecordJoinButtonPressedFromUpNextView(event);
}
void CalendarViewController::OnCalendarEventWillLaunch() {
UmaHistogramMediumTimes("Ash.Calendar.UserJourneyTime.EventLaunched",
base::TimeTicks::Now() - calendar_open_time_);

@ -77,6 +77,12 @@ class ASH_EXPORT CalendarViewController {
// is used currently).
void RecordEventListItemActivated(const ui::Event& event);
// Records a metric for the "Join" meeting button being pressed.
// Captures whether it was from the `CalendarEventListView` or implicitly the
// `CalendarUpNextView` (the only other place the `CalendarEventListItemView`
// is used currently).
void RecordJoinMeetingButtonPressed(const ui::Event& event);
// Called when a calendar event is about to launch. Used to record metrics.
void OnCalendarEventWillLaunch();

@ -250,4 +250,33 @@ TEST_F(CalendarViewControllerUnittest, MaxDistanceBrowsedRecordedOnClose) {
histogram_tester.ExpectTotalCount("Ash.Calendar.MaxDistanceBrowsed", 1);
}
TEST_F(
CalendarViewControllerUnittest,
ShouldRecordEventListViewJoinMeetingButtonPressed_WhenEventListIsShowing) {
base::HistogramTester histogram_tester;
auto controller = std::make_unique<CalendarViewController>();
controller->OnEventListOpened();
const std::unique_ptr<ui::Event> test_event = std::make_unique<ui::KeyEvent>(
ui::EventType::ET_MOUSE_PRESSED, ui::VKEY_UNKNOWN, ui::EF_NONE);
controller->RecordJoinMeetingButtonPressed(*test_event);
histogram_tester.ExpectTotalCount(
"Ash.Calendar.EventListView.JoinMeetingButton.Pressed", 1);
}
TEST_F(
CalendarViewControllerUnittest,
ShouldRecordUpNextViewJoinMeetingButtonPressed_WhenEventListIsNotShowing) {
base::HistogramTester histogram_tester;
auto controller = std::make_unique<CalendarViewController>();
const std::unique_ptr<ui::Event> test_event = std::make_unique<ui::KeyEvent>(
ui::EventType::ET_MOUSE_PRESSED, ui::VKEY_UNKNOWN, ui::EF_NONE);
controller->RecordJoinMeetingButtonPressed(*test_event);
histogram_tester.ExpectTotalCount(
"Ash.Calendar.UpNextView.JoinMeetingButton.Pressed", 1);
}
} // namespace ash

@ -744,13 +744,33 @@ void SystemTrayClientImpl::ShowCalendarEvent(
}
// Launch web app.
proxy->LaunchAppWithUrl(web_app::kGoogleCalendarAppId,
apps::GetEventFlags(WindowOpenDisposition::NEW_WINDOW,
/*prefer_container=*/true),
proxy->LaunchAppWithUrl(web_app::kGoogleCalendarAppId, ui::EF_NONE,
official_url, apps::LaunchSource::kFromShelf);
opened_pwa = true;
}
// TODO(b/269075177): Reuse existing Google Meet PWA instead of opening a new
// one for each call to `LaunchAppWithUrl`.
void SystemTrayClientImpl::ShowGoogleMeet(const std::string& hangout_link) {
const auto final_url = GURL(hangout_link);
if (!IsAppInstalled(web_app::kGoogleMeetAppId)) {
OpenInBrowser(final_url);
return;
}
apps::AppServiceProxyAsh* proxy = GetActiveUserAppServiceProxyAsh();
if (!proxy) {
LOG(ERROR) << __FUNCTION__
<< " failed to get active user AppServiceProxyAsh";
OpenInBrowser(final_url);
return;
}
proxy->LaunchAppWithUrl(web_app::kGoogleMeetAppId, ui::EF_NONE, final_url,
apps::LaunchSource::kFromShelf);
}
void SystemTrayClientImpl::ShowChannelInfoAdditionalDetails() {
base::RecordAction(
base::UserMetricsAction("Tray_ShowChannelInfoAdditionalDetails"));

@ -101,6 +101,7 @@ class SystemTrayClientImpl : public ash::SystemTrayClient,
const base::Time& date,
bool& opened_pwa,
GURL& finalized_event_url) override;
void ShowGoogleMeet(const std::string& hangout_link) override;
void ShowChannelInfoAdditionalDetails() override;
void ShowChannelInfoGiveFeedback() override;
void ShowAudioSettings() override;

@ -558,6 +558,50 @@ IN_PROC_BROWSER_TEST_F(SystemTrayClientShowCalendarTest, UnofficialEventUrl) {
EXPECT_EQ(final_url.spec(), GURL(kOfficialCalendarEventUrl).spec());
}
class SystemTrayClientShowGoogleMeetTest
: public SystemTrayClientShowCalendarTest {
public:
SystemTrayClientShowGoogleMeetTest() = default;
~SystemTrayClientShowGoogleMeetTest() override = default;
protected:
// ash::LoginManagerTest:
void SetUpOnMainThread() override {
ash::LoginManagerTest::SetUpOnMainThread();
LoginUser(account_id_);
browser_ = CreateBrowser(
ash::ProfileHelper::Get()->GetProfileByAccountId(account_id_));
ASSERT_TRUE(browser_);
}
Browser* browser_ = nullptr;
};
IN_PROC_BROWSER_TEST_F(SystemTrayClientShowGoogleMeetTest,
LaunchGoogleMeetInBrowser) {
constexpr char kMeetUrl[] = "https://meet.google.com/abc-123";
ash::Shell::Get()->system_tray_model()->client()->ShowGoogleMeet(kMeetUrl);
EXPECT_EQ(
GURL(kMeetUrl),
browser_->tab_strip_model()->GetActiveWebContents()->GetVisibleURL());
}
IN_PROC_BROWSER_TEST_F(SystemTrayClientShowGoogleMeetTest,
LaunchGoogleMeetInGoogleMeetApp) {
constexpr char kMeetUrl[] = "https://meet.google.com/abc-123";
InstallApp(web_app::kGoogleMeetAppId, "Google Meet");
ash::Shell::Get()->system_tray_model()->client()->ShowGoogleMeet(kMeetUrl);
// Expect the meet_url not to have opened in the browser.
EXPECT_NE(
GURL(kMeetUrl),
browser_->tab_strip_model()->GetActiveWebContents()->GetVisibleURL());
}
class SystemTrayClientShowChannelInfoGiveFeedbackTest
: public ash::LoginManagerTest {
public:

@ -33,7 +33,8 @@ constexpr int kMaxResults = 2500;
constexpr char kCalendarEventListFields[] =
"timeZone,etag,kind,items(id,kind,summary,colorId,"
"status,start(date),end(date),start(dateTime),end(dateTime),htmlLink,"
"attendees(responseStatus,self),attendeesOmitted,creator(self))";
"attendees(responseStatus,self),attendeesOmitted,hangoutLink,"
"creator(self))";
CalendarApiGetRequest::CalendarApiGetRequest(RequestSender* sender,
const std::string& fields)

@ -112,6 +112,7 @@ TEST_F(CalendarApiRequestsTest, GetEventListRequest) {
"2Cstart(date)%2Cend(date)%"
"2Cstart(dateTime)%2Cend(dateTime)%"
"2ChtmlLink%2Cattendees(responseStatus%2Cself)%2CattendeesOmitted%"
"2ChangoutLink%"
"2Ccreator(self))",
http_request_.relative_url);

@ -44,6 +44,7 @@ constexpr char kAttendeesResponseStatus[] = "responseStatus";
constexpr char kAttendeesSelf[] = "self";
constexpr char kCalendarEventKind[] = "calendar#event";
constexpr char kColorId[] = "colorId";
constexpr char kHangoutLink[] = "hangoutLink";
constexpr char kEnd[] = "end";
constexpr char kHtmlLink[] = "htmlLink";
constexpr char kPathToCreatorSelf[] = "creator.self";
@ -64,27 +65,6 @@ constexpr auto kAttendeesResponseStatuses =
{"needsAction", CalendarEvent::ResponseStatus::kNeedsAction},
{"tentative", CalendarEvent::ResponseStatus::kTentative}});
// Converts the `items` field from the response. This method helps to use the
// custom conversion entrypoint `CalendarEvent::CreateFrom`.
// Returns false when it fails (e.g. the value is structurally different from
// expected).
bool ConvertResponseItems(const base::Value* value,
std::vector<std::unique_ptr<CalendarEvent>>* result) {
const auto* items = value->GetIfList();
if (!items)
return false;
result->reserve(items->size());
for (const auto& item : *items) {
auto event = CalendarEvent::CreateFrom(item);
if (!event)
return false;
result->push_back(std::move(event));
}
return true;
}
// Converts the event status to `EventStatus`. Returns false when it fails
// (e.g. the value is structurally different from expected).
bool ConvertEventStatus(const base::Value* value,
@ -168,6 +148,30 @@ absl::optional<CalendarEvent::ResponseStatus> CalculateSelfResponseStatus(
return CalendarEvent::ResponseStatus::kUnknown;
}
// Converts the `items` field from the response. This method helps to use the
// custom conversion entrypoint `CalendarEvent::CreateFrom`.
// Returns false when it fails (e.g. the value is structurally different from
// expected).
bool ConvertResponseItems(const base::Value* value, CalendarEvent* event) {
base::JSONValueConverter<CalendarEvent> converter;
if (!IsResourceKindExpected(*value, kCalendarEventKind) ||
!converter.Convert(*value, event)) {
DVLOG(1) << "Unable to create: Invalid CalendarEvent JSON!";
return false;
}
auto self_response_status = CalculateSelfResponseStatus(*value);
if (self_response_status.has_value()) {
event->set_self_response_status(self_response_status.value());
return true;
}
DVLOG(1) << "Unable to calculate self response status: Invalid "
"CalendarEvent JSON!";
return false;
}
bool IsAllDayEvent(const base::Value* value, bool* result) {
*result = value->GetDict().Find("date") != nullptr;
return result;
@ -218,6 +222,7 @@ void CalendarEvent::RegisterJSONConverter(
converter->RegisterStringField(kSummary, &CalendarEvent::summary_);
converter->RegisterStringField(kHtmlLink, &CalendarEvent::html_link_);
converter->RegisterStringField(kColorId, &CalendarEvent::color_id_);
converter->RegisterStringField(kHangoutLink, &CalendarEvent::hangout_link_);
converter->RegisterCustomValueField(kStatus, &CalendarEvent::status_,
&ConvertEventStatus);
converter->RegisterCustomValueField(kStart, &CalendarEvent::start_time_,
@ -228,28 +233,6 @@ void CalendarEvent::RegisterJSONConverter(
&IsAllDayEvent);
}
// static
std::unique_ptr<CalendarEvent> CalendarEvent::CreateFrom(
const base::Value& value) {
auto event = std::make_unique<CalendarEvent>();
base::JSONValueConverter<CalendarEvent> converter;
if (!IsResourceKindExpected(value, kCalendarEventKind) ||
!converter.Convert(value, event.get())) {
DVLOG(1) << "Unable to create: Invalid CalendarEvent JSON!";
return nullptr;
}
auto self_response_status = CalculateSelfResponseStatus(value);
if (self_response_status.has_value()) {
event->set_self_response_status(self_response_status.value());
return event;
}
DVLOG(1) << "Unable to calculate self response status: Invalid "
"CalendarEvent JSON!";
return nullptr;
}
int CalendarEvent::GetApproximateSizeInBytes() const {
int total_bytes = 0;
@ -260,6 +243,7 @@ int CalendarEvent::GetApproximateSizeInBytes() const {
total_bytes += color_id_.length();
total_bytes += sizeof(status_);
total_bytes += sizeof(self_response_status_);
total_bytes += hangout_link_.length();
return total_bytes;
}
@ -274,8 +258,8 @@ void EventList::RegisterJSONConverter(
converter->RegisterStringField(kTimeZone, &EventList::time_zone_);
converter->RegisterStringField(kApiResponseETagKey, &EventList::etag_);
converter->RegisterStringField(kApiResponseKindKey, &EventList::kind_);
converter->RegisterCustomValueField(kApiResponseItemsKey, &EventList::items_,
&ConvertResponseItems);
converter->RegisterRepeatedCustomValue<CalendarEvent>(
kApiResponseItemsKey, &EventList::items_, &ConvertResponseItems);
}
// static

@ -82,9 +82,6 @@ class CalendarEvent {
static void RegisterJSONConverter(
base::JSONValueConverter<CalendarEvent>* converter);
// Creates CalendarEvent from parsed JSON.
static std::unique_ptr<CalendarEvent> CreateFrom(const base::Value& value);
// The ID of this Calendar Event.
const std::string& id() const { return id_; }
void set_id(const std::string& id) { id_ = id; }
@ -122,6 +119,12 @@ class CalendarEvent {
all_day_event_ = all_day_event;
}
// Google Meet video conference URL, if one is attached to the event.
const std::string& hangout_link() const { return hangout_link_; }
void set_hangout_link(const std::string& hangout_link) {
hangout_link_ = hangout_link;
}
// Return the approximate size of this event, in bytes.
int GetApproximateSizeInBytes() const;
@ -134,7 +137,8 @@ class CalendarEvent {
ResponseStatus self_response_status_ = ResponseStatus::kUnknown;
DateTime start_time_;
DateTime end_time_;
bool all_day_event_;
bool all_day_event_ = false;
std::string hangout_link_;
};
// Parses a list of calendar events.

@ -45,6 +45,7 @@ TEST(CalendarAPIResponseTypesTest, ParseEventList) {
EXPECT_EQ(event.status(), CalendarEvent::EventStatus::kConfirmed);
EXPECT_EQ(event.self_response_status(),
CalendarEvent::ResponseStatus::kNeedsAction);
EXPECT_EQ(event.hangout_link(), "https://meet.google.com/jbe-test");
}
TEST(CalendarAPIResponseTypesTest, ParseEventListWithCorrectEventStatuses) {

@ -810,6 +810,18 @@ chromium-metrics-reviews@google.com.
</summary>
</histogram>
<histogram name="Ash.Calendar.EventListView.JoinMeetingButton.Pressed"
enum="CalendarEventSource" expires_after="2024-02-10">
<owner>newcomer@google.com</owner>
<owner>cros-status-area-eng@google.com</owner>
<summary>
Recorded when the &quot;Join&quot; meeting button is pressed from the
Calendar &quot;EventListView&quot; i.e. the user opens the sys tray
calendar, taps todays date cell, then scrolls to the event and taps the
&quot;Join&quot; meeting button.
</summary>
</histogram>
<histogram name="Ash.Calendar.FetchEvents.FetchDuration" units="ms"
expires_after="2023-09-18">
<owner>rtinkoff@google.com</owner>
@ -985,6 +997,18 @@ chromium-metrics-reviews@google.com.
</summary>
</histogram>
<histogram name="Ash.Calendar.UpNextView.JoinMeetingButton.Pressed"
enum="CalendarEventSource" expires_after="2024-02-10">
<owner>newcomer@google.com</owner>
<owner>cros-status-area-eng@google.com</owner>
<summary>
Recorded when the &quot;Join&quot; meeting button is pressed from the
Calendar &quot;Up next&quot; view i.e. the user opens the sys tray calendar,
&quot;Up next&quot; displays an event in the next 10 mins and the user taps
&quot;Join&quot; meeting button on the event.
</summary>
</histogram>
<histogram name="Ash.Calendar.UserJourneyTime.{EventLaunchState}" units="ms"
expires_after="2023-09-18">
<owner>newcomer@google.com</owner>