Commit 90f6ff2e authored by Tommy Steimel's avatar Tommy Steimel Committed by Commit Bot

GMC: Dismiss paused sessions after an hour of inactivity

This CL adds logic to MediaNotificationService::Session to dismiss its
MediaNotificationItem after an hour of inactivity when paused.

Bug: 1025439
Change-Id: Ie2374b1f25bcbf332dace4ffa1220507d22f1a73
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1920017
Commit-Queue: Tommy Steimel <steimel@chromium.org>
Reviewed-by: default avatarMounir Lamouri <mlamouri@chromium.org>
Cr-Commit-Position: refs/heads/master@{#716343}
parent b9761b67
...@@ -26,6 +26,9 @@ ...@@ -26,6 +26,9 @@
namespace { namespace {
constexpr base::TimeDelta kInactiveTimerDelay =
base::TimeDelta::FromMinutes(60);
// Here we check to see if the WebContents is focused. Note that since Session // Here we check to see if the WebContents is focused. Note that since Session
// is a WebContentsObserver, we could in theory listen for // is a WebContentsObserver, we could in theory listen for
// |OnWebContentsFocused()| and |OnWebContentsLostFocus()|. However, this won't // |OnWebContentsFocused()| and |OnWebContentsLostFocus()|. However, this won't
...@@ -52,13 +55,16 @@ MediaNotificationService::Session::Session( ...@@ -52,13 +55,16 @@ MediaNotificationService::Session::Session(
MediaNotificationService* owner, MediaNotificationService* owner,
const std::string& id, const std::string& id,
std::unique_ptr<media_message_center::MediaSessionNotificationItem> item, std::unique_ptr<media_message_center::MediaSessionNotificationItem> item,
content::WebContents* web_contents) content::WebContents* web_contents,
mojo::Remote<media_session::mojom::MediaController> controller)
: content::WebContentsObserver(web_contents), : content::WebContentsObserver(web_contents),
owner_(owner), owner_(owner),
id_(id), id_(id),
item_(std::move(item)) { item_(std::move(item)) {
DCHECK(owner_); DCHECK(owner_);
DCHECK(item_); DCHECK(item_);
SetController(std::move(controller));
} }
MediaNotificationService::Session::~Session() = default; MediaNotificationService::Session::~Session() = default;
...@@ -69,6 +75,67 @@ void MediaNotificationService::Session::WebContentsDestroyed() { ...@@ -69,6 +75,67 @@ void MediaNotificationService::Session::WebContentsDestroyed() {
owner_->RemoveItem(id_); owner_->RemoveItem(id_);
} }
void MediaNotificationService::Session::OnWebContentsFocused(
content::RenderWidgetHost*) {
OnSessionInteractedWith();
}
void MediaNotificationService::Session::MediaSessionInfoChanged(
media_session::mojom::MediaSessionInfoPtr session_info) {
bool playing =
session_info && session_info->playback_state ==
media_session::mojom::MediaPlaybackState::kPlaying;
// If we've started playing, we don't want the inactive timer to be running.
if (playing) {
inactive_timer_.Stop();
return;
}
// If the timer is already running, we don't need to do anything.
if (inactive_timer_.IsRunning())
return;
StartInactiveTimer();
}
void MediaNotificationService::Session::MediaSessionPositionChanged(
const base::Optional<media_session::MediaPosition>& position) {
OnSessionInteractedWith();
}
void MediaNotificationService::Session::SetController(
mojo::Remote<media_session::mojom::MediaController> controller) {
if (controller.is_bound()) {
observer_receiver_.reset();
controller->AddObserver(observer_receiver_.BindNewPipeAndPassRemote());
}
}
void MediaNotificationService::Session::OnSessionInteractedWith() {
// If we're not currently tracking inactive time, then no action is needed.
if (!inactive_timer_.IsRunning())
return;
// Otherwise, reset the timer.
inactive_timer_.Stop();
StartInactiveTimer();
}
void MediaNotificationService::Session::StartInactiveTimer() {
DCHECK(!inactive_timer_.IsRunning());
inactive_timer_.Start(
FROM_HERE, kInactiveTimerDelay,
base::BindOnce(
[](media_message_center::MediaSessionNotificationItem* item) {
// If the session has been paused and inactive for long enough, then
// dismiss it.
item->Dismiss();
},
item_.get()));
}
MediaNotificationService::MediaNotificationService( MediaNotificationService::MediaNotificationService(
Profile* profile, Profile* profile,
service_manager::Connector* connector) service_manager::Connector* connector)
...@@ -132,19 +199,23 @@ void MediaNotificationService::OnFocusGained( ...@@ -132,19 +199,23 @@ void MediaNotificationService::OnFocusGained(
if (it != sessions_.end() && !it->second.item()->frozen()) if (it != sessions_.end() && !it->second.item()->frozen())
return; return;
mojo::Remote<media_session::mojom::MediaController> controller; mojo::Remote<media_session::mojom::MediaController> item_controller;
mojo::Remote<media_session::mojom::MediaController> session_controller;
// |controller_manager_remote_| may be null in tests where connector is // |controller_manager_remote_| may be null in tests where connector is
// unavailable. // unavailable.
if (controller_manager_remote_) { if (controller_manager_remote_) {
controller_manager_remote_->CreateMediaControllerForSession( controller_manager_remote_->CreateMediaControllerForSession(
controller.BindNewPipeAndPassReceiver(), *session->request_id); item_controller.BindNewPipeAndPassReceiver(), *session->request_id);
controller_manager_remote_->CreateMediaControllerForSession(
session_controller.BindNewPipeAndPassReceiver(), *session->request_id);
} }
if (it != sessions_.end()) { if (it != sessions_.end()) {
// If the notification was previously frozen then we should reset the // If the notification was previously frozen then we should reset the
// controller because the mojo pipe would have been reset. // controller because the mojo pipe would have been reset.
it->second.item()->SetController(std::move(controller), it->second.SetController(std::move(session_controller));
it->second.item()->SetController(std::move(item_controller),
std::move(session->session_info)); std::move(session->session_info));
if (!base::Contains(dragged_out_session_ids_, id)) if (!base::Contains(dragged_out_session_ids_, id))
active_controllable_session_ids_.insert(id); active_controllable_session_ids_.insert(id);
...@@ -159,9 +230,10 @@ void MediaNotificationService::OnFocusGained( ...@@ -159,9 +230,10 @@ void MediaNotificationService::OnFocusGained(
std::make_unique< std::make_unique<
media_message_center::MediaSessionNotificationItem>( media_message_center::MediaSessionNotificationItem>(
this, id, session->source_name.value_or(std::string()), this, id, session->source_name.value_or(std::string()),
std::move(controller), std::move(session->session_info)), std::move(item_controller), std::move(session->session_info)),
content::MediaSession::GetWebContentsFromRequestId( content::MediaSession::GetWebContentsFromRequestId(
*session->request_id))); *session->request_id),
std::move(session_controller)));
} }
} }
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
#include "base/macros.h" #include "base/macros.h"
#include "base/memory/weak_ptr.h" #include "base/memory/weak_ptr.h"
#include "base/timer/timer.h"
#include "chrome/browser/ui/global_media_controls/cast_media_notification_provider.h" #include "chrome/browser/ui/global_media_controls/cast_media_notification_provider.h"
#include "chrome/browser/ui/global_media_controls/media_notification_container_observer.h" #include "chrome/browser/ui/global_media_controls/media_notification_container_observer.h"
#include "chrome/browser/ui/global_media_controls/overlay_media_notifications_manager.h" #include "chrome/browser/ui/global_media_controls/overlay_media_notifications_manager.h"
...@@ -99,28 +100,62 @@ class MediaNotificationService ...@@ -99,28 +100,62 @@ class MediaNotificationService
friend class MediaNotificationServiceTest; friend class MediaNotificationServiceTest;
friend class MediaToolbarButtonControllerTest; friend class MediaToolbarButtonControllerTest;
class Session : public content::WebContentsObserver { class Session : public content::WebContentsObserver,
public media_session::mojom::MediaControllerObserver {
public: public:
Session(MediaNotificationService* owner, Session(MediaNotificationService* owner,
const std::string& id, const std::string& id,
std::unique_ptr<media_message_center::MediaSessionNotificationItem> std::unique_ptr<media_message_center::MediaSessionNotificationItem>
item, item,
content::WebContents* web_contents); content::WebContents* web_contents,
mojo::Remote<media_session::mojom::MediaController> controller);
Session(const Session&) = delete; Session(const Session&) = delete;
Session& operator=(const Session&) = delete; Session& operator=(const Session&) = delete;
~Session() override; ~Session() override;
// content::WebContentsObserver implementation. // content::WebContentsObserver implementation.
void WebContentsDestroyed() override; void WebContentsDestroyed() override;
void OnWebContentsFocused(content::RenderWidgetHost*) override;
// media_session::mojom::MediaControllerObserver:
void MediaSessionInfoChanged(
media_session::mojom::MediaSessionInfoPtr session_info) override;
void MediaSessionMetadataChanged(
const base::Optional<media_session::MediaMetadata>& metadata) override {
}
void MediaSessionActionsChanged(
const std::vector<media_session::mojom::MediaSessionAction>& actions)
override {}
void MediaSessionChanged(
const base::Optional<base::UnguessableToken>& request_id) override {}
void MediaSessionPositionChanged(
const base::Optional<media_session::MediaPosition>& position) override;
media_message_center::MediaSessionNotificationItem* item() { media_message_center::MediaSessionNotificationItem* item() {
return item_.get(); return item_.get();
} }
// Called when a new MediaController is given to the item. We need to
// observe the same session as our underlying item.
void SetController(
mojo::Remote<media_session::mojom::MediaController> controller);
private: private:
void StartInactiveTimer();
// Called when a session is interacted with (to reset |inactive_timer_|).
void OnSessionInteractedWith();
MediaNotificationService* owner_; MediaNotificationService* owner_;
const std::string id_; const std::string id_;
std::unique_ptr<media_message_center::MediaSessionNotificationItem> item_; std::unique_ptr<media_message_center::MediaSessionNotificationItem> item_;
// Used to stop/hide a paused session after a period of inactivity.
base::OneShotTimer inactive_timer_;
// Used to receive updates to the Media Session playback state.
mojo::Receiver<media_session::mojom::MediaControllerObserver>
observer_receiver_{this};
}; };
void OnReceivedAudioFocusRequests( void OnReceivedAudioFocusRequests(
......
...@@ -134,6 +134,10 @@ class MediaNotificationServiceTest : public testing::Test { ...@@ -134,6 +134,10 @@ class MediaNotificationServiceTest : public testing::Test {
base::TimeDelta::FromMilliseconds(milliseconds)); base::TimeDelta::FromMilliseconds(milliseconds));
} }
void AdvanceClockMinutes(int minutes) {
AdvanceClockMilliseconds(1000 * 60 * minutes);
}
base::UnguessableToken SimulatePlayingControllableMedia() { base::UnguessableToken SimulatePlayingControllableMedia() {
base::UnguessableToken id = base::UnguessableToken::Create(); base::UnguessableToken id = base::UnguessableToken::Create();
SimulateFocusGained(id, true); SimulateFocusGained(id, true);
...@@ -236,6 +240,31 @@ class MediaNotificationServiceTest : public testing::Test { ...@@ -236,6 +240,31 @@ class MediaNotificationServiceTest : public testing::Test {
item_itr->second.WebContentsDestroyed(); item_itr->second.WebContentsDestroyed();
} }
void SimulateTabFocused(const base::UnguessableToken& id) {
auto item_itr = service_->sessions_.find(id.ToString());
EXPECT_NE(service_->sessions_.end(), item_itr);
item_itr->second.OnWebContentsFocused(nullptr);
}
void SimulatePlaybackStateChanged(const base::UnguessableToken& id,
bool playing) {
MediaSessionInfoPtr session_info(MediaSessionInfo::New());
session_info->is_controllable = true;
session_info->playback_state =
playing ? media_session::mojom::MediaPlaybackState::kPlaying
: media_session::mojom::MediaPlaybackState::kPaused;
auto item_itr = service_->sessions_.find(id.ToString());
EXPECT_NE(service_->sessions_.end(), item_itr);
item_itr->second.MediaSessionInfoChanged(std::move(session_info));
}
void SimulateMediaSeeked(const base::UnguessableToken& id) {
auto item_itr = service_->sessions_.find(id.ToString());
EXPECT_NE(service_->sessions_.end(), item_itr);
item_itr->second.MediaSessionPositionChanged(base::nullopt);
}
void SimulateDismissButtonClicked(const base::UnguessableToken& id) { void SimulateDismissButtonClicked(const base::UnguessableToken& id) {
service_->OnContainerDismissed(id.ToString()); service_->OnContainerDismissed(id.ToString());
} }
...@@ -584,3 +613,75 @@ TEST_F(MediaNotificationServiceTest, ShowsOverlayForDraggedOutNotifications) { ...@@ -584,3 +613,75 @@ TEST_F(MediaNotificationServiceTest, ShowsOverlayForDraggedOutNotifications) {
manager->OnOverlayNotificationClosed(id.ToString()); manager->OnOverlayNotificationClosed(id.ToString());
testing::Mock::VerifyAndClearExpectations(&dialog_delegate); testing::Mock::VerifyAndClearExpectations(&dialog_delegate);
} }
TEST_F(MediaNotificationServiceTest, HidesInactiveNotifications) {
// Start playing active media.
base::UnguessableToken id = SimulatePlayingControllableMedia();
EXPECT_TRUE(HasActiveNotifications());
// Then, pause the media. We should still have the active notification.
SimulatePlaybackStateChanged(id, false);
EXPECT_TRUE(HasActiveNotifications());
// After 59 minutes, the notification should still be there.
AdvanceClockMinutes(59);
EXPECT_TRUE(HasActiveNotifications());
// But once it's been inactive for over an hour, it should disappear.
AdvanceClockMinutes(2);
EXPECT_FALSE(HasActiveNotifications());
}
TEST_F(MediaNotificationServiceTest, DelaysHidingNotifications_PlayPause) {
// Start playing active media.
base::UnguessableToken id = SimulatePlayingControllableMedia();
EXPECT_TRUE(HasActiveNotifications());
// Then, pause the media. We should still have the active notification.
SimulatePlaybackStateChanged(id, false);
EXPECT_TRUE(HasActiveNotifications());
// After 59 minutes, the notification should still be there.
AdvanceClockMinutes(59);
EXPECT_TRUE(HasActiveNotifications());
// If we start playing again, we should not hide the notification, even after
// an hour.
SimulatePlaybackStateChanged(id, true);
AdvanceClockMinutes(2);
EXPECT_TRUE(HasActiveNotifications());
// If we pause again, it should hide after an hour.
SimulatePlaybackStateChanged(id, false);
AdvanceClockMinutes(61);
EXPECT_FALSE(HasActiveNotifications());
}
TEST_F(MediaNotificationServiceTest, DelaysHidingNotifications_Interactions) {
// Start playing active media.
base::UnguessableToken id = SimulatePlayingControllableMedia();
EXPECT_TRUE(HasActiveNotifications());
// Then, pause the media. We should still have the active notification.
SimulatePlaybackStateChanged(id, false);
EXPECT_TRUE(HasActiveNotifications());
// After 59 minutes, the notification should still be there.
AdvanceClockMinutes(59);
EXPECT_TRUE(HasActiveNotifications());
// If the user goes back to the tab, it should reset the hide timer.
SimulateTabFocused(id);
AdvanceClockMinutes(59);
EXPECT_TRUE(HasActiveNotifications());
// If the user seeks the media before an hour is up, it should reset the hide
// timer.
SimulateMediaSeeked(id);
AdvanceClockMinutes(59);
EXPECT_TRUE(HasActiveNotifications());
// After the hour has passed, the notification should hide.
AdvanceClockMinutes(2);
EXPECT_FALSE(HasActiveNotifications());
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment