Commit 019d2e02 authored by Chris Hamilton's avatar Chris Hamilton Committed by Commit Bot

Create RecentlyAudibleHelper.

A follow-up CL will refactor existing consumers of
WC::WasRecentlyAudible to use this API instead, before removing the
relevant content API. More details in the bug.

BUG=846374

Change-Id: I7a758b7fc4d01ca7e0a98676978d1de333829866
Reviewed-on: https://chromium-review.googlesource.com/1072748
Commit-Queue: Chris Hamilton <chrisha@chromium.org>
Reviewed-by: default avatarJohn Abd-El-Malek <jam@chromium.org>
Cr-Commit-Position: refs/heads/master@{#561848}
parent d5bd60f8
...@@ -694,6 +694,8 @@ split_static_library("ui") { ...@@ -694,6 +694,8 @@ split_static_library("ui") {
"profile_error_dialog.h", "profile_error_dialog.h",
"protocol_dialog_delegate.h", "protocol_dialog_delegate.h",
"proximity_auth/proximity_auth_error_bubble.h", "proximity_auth/proximity_auth_error_bubble.h",
"recently_audible_helper.cc",
"recently_audible_helper.h",
"screen_capture_notification_ui.h", "screen_capture_notification_ui.h",
"search_engines/edit_search_engine_controller.cc", "search_engines/edit_search_engine_controller.cc",
"search_engines/edit_search_engine_controller.h", "search_engines/edit_search_engine_controller.h",
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/recently_audible_helper.h"
#include "base/time/default_tick_clock.h"
namespace {
const base::TickClock* GetDefaultTickClock() {
static base::DefaultTickClock default_tick_clock;
return &default_tick_clock;
}
} // namespace
DEFINE_WEB_CONTENTS_USER_DATA_KEY(RecentlyAudibleHelper);
// static
constexpr base::TimeDelta RecentlyAudibleHelper::kRecentlyAudibleTimeout;
RecentlyAudibleHelper::~RecentlyAudibleHelper() = default;
bool RecentlyAudibleHelper::WasEverAudible() const {
return !last_audible_time_.is_null();
}
bool RecentlyAudibleHelper::IsCurrentlyAudible() const {
return last_audible_time_.is_max();
}
bool RecentlyAudibleHelper::WasRecentlyAudible() const {
if (last_audible_time_.is_max())
return true;
if (last_audible_time_.is_null())
return false;
base::TimeTicks recently_audible_time_limit =
last_audible_time_ + kRecentlyAudibleTimeout;
return tick_clock_->NowTicks() < recently_audible_time_limit;
}
std::unique_ptr<RecentlyAudibleHelper::Subscription>
RecentlyAudibleHelper::RegisterCallback(const Callback& callback) {
return callback_list_.Add(callback);
}
RecentlyAudibleHelper::RecentlyAudibleHelper(content::WebContents* contents)
: content::WebContentsObserver(contents),
tick_clock_(GetDefaultTickClock()) {}
void RecentlyAudibleHelper::OnAudioStateChanged(bool audible) {
// If audio is stopping remember the time at which it stopped and set a timer
// to fire the recently audible transition.
if (!audible) {
last_audible_time_ = tick_clock_->NowTicks();
recently_audible_timer_.Start(
FROM_HERE, kRecentlyAudibleTimeout, this,
&RecentlyAudibleHelper::OnRecentlyAudibleTimerFired);
return;
}
// If the tab was not recently audible prior to the audio starting then notify
// that it has become recently audible again. Otherwise, swallow this
// notification.
bool was_recently_audible = WasRecentlyAudible();
last_audible_time_ = base::TimeTicks::Max();
recently_audible_timer_.Stop();
if (!was_recently_audible)
callback_list_.Notify(true);
}
void RecentlyAudibleHelper::OnRecentlyAudibleTimerFired() {
DCHECK(last_audible_time_ + kRecentlyAudibleTimeout >=
tick_clock_->NowTicks());
// Notify of the transition to no longer being recently audible.
callback_list_.Notify(false);
}
void RecentlyAudibleHelper::SetTickClockForTesting(
const base::TickClock* tick_clock) {
if (tick_clock) {
tick_clock_ = tick_clock;
} else {
tick_clock_ = GetDefaultTickClock();
}
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_UI_RECENTLY_AUDIBLE_HELPER_H_
#define CHROME_BROWSER_UI_RECENTLY_AUDIBLE_HELPER_H_
#include "base/callback_list.h"
#include "base/macros.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/browser/web_contents_user_data.h"
namespace base {
class TickClock;
}
// A helper that observers tab audibility and calculates whether or not a tab
// is recently audible. This is used to make the "audio playing" icon persist
// for a short period after audio stops. This class is only safe to use from the
// UI thread.
class RecentlyAudibleHelper
: public content::WebContentsObserver,
public content::WebContentsUserData<RecentlyAudibleHelper> {
public:
// This corresponds to the amount of time that the "audio playing" icon will
// persist in the tab strip after audio has stopped playing.
static constexpr base::TimeDelta kRecentlyAudibleTimeout =
base::TimeDelta::FromSeconds(2);
using CallbackList = base::CallbackList<void(bool was_recently_audible)>;
using Callback = CallbackList::CallbackType;
using Subscription = CallbackList::Subscription;
~RecentlyAudibleHelper() override;
// Returns true if the WebContents was ever audible over its lifetime.
bool WasEverAudible() const;
// Returns true if the WebContents is currently audible.
bool IsCurrentlyAudible() const;
// Returns true if the WebContents is currently audible, or was audible
// recently.
bool WasRecentlyAudible() const;
// Registers the provided repeating callback for notifications. Destroying
// the returned subscription will unregister the callback. This is safe to do
// while in the context of the callback itself.
std::unique_ptr<Subscription> RegisterCallback(const Callback& callback);
// Allows replacing the tick clock that is used by this class. Setting it back
// to nullptr will restore the default tick clock.
void SetTickClockForTesting(const base::TickClock* tick_clock);
private:
friend class RecentlyAudibleHelperTest;
friend class content::WebContentsUserData<RecentlyAudibleHelper>;
explicit RecentlyAudibleHelper(content::WebContents* contents);
// contents::WebContentsObserver implementation:
void OnAudioStateChanged(bool audible) override;
// The callback that is invoked by the |recently_audible_timer_|.
void OnRecentlyAudibleTimerFired();
// is_null() if the tab has never been audible, and is_max() if audio is
// currently playing. Otherwise, corresponds to the last time the tab was
// audible.
base::TimeTicks last_audible_time_;
// Timer for determining when "recently audible" transitions to false. This
// starts running when a tab stops being audible, and is canceled if it starts
// being audible again before it fires.
base::OneShotTimer recently_audible_timer_;
// List of callbacks observing this helper.
CallbackList callback_list_;
// The tick clock this object is using.
const base::TickClock* tick_clock_;
DISALLOW_COPY_AND_ASSIGN(RecentlyAudibleHelper);
};
#endif // CHROME_BROWSER_UI_RECENTLY_AUDIBLE_HELPER_H_
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/recently_audible_helper.h"
#include <list>
#include "base/bind.h"
#include "base/message_loop/message_loop_current.h"
#include "base/run_loop.h"
#include "base/test/test_mock_time_task_runner.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "content/public/test/test_web_contents_factory.h"
#include "content/public/test/web_contents_tester.h"
#include "testing/gtest/include/gtest/gtest.h"
class RecentlyAudibleHelperTest : public testing::Test {
public:
RecentlyAudibleHelperTest() = default;
~RecentlyAudibleHelperTest() override {}
void SetUp() override {
test_web_contents_factory_.reset(new content::TestWebContentsFactory);
contents_ =
test_web_contents_factory_->CreateWebContents(&testing_profile_);
// Replace the main message loop with one that uses mock time.
scoped_context_ =
std::make_unique<base::TestMockTimeTaskRunner::ScopedContext>(
task_runner_);
base::MessageLoopCurrent::Get()->SetTaskRunner(task_runner_);
RecentlyAudibleHelper::CreateForWebContents(contents_);
helper_ = RecentlyAudibleHelper::FromWebContents(contents_);
helper_->SetTickClockForTesting(task_runner_->GetMockTickClock());
subscription_ = helper_->RegisterCallback(base::BindRepeating(
&RecentlyAudibleHelperTest::OnRecentlyAudibleCallback,
base::Unretained(this)));
}
void TearDown() override {
helper_->SetTickClockForTesting(nullptr);
subscription_.reset();
task_runner_->RunUntilIdle();
EXPECT_TRUE(recently_audible_messages_.empty());
scoped_context_.reset();
test_web_contents_factory_.reset();
}
void SimulateAudioStarts() {
content::WebContentsTester::For(contents_)->SetIsCurrentlyAudible(true);
helper_->OnAudioStateChanged(true);
}
void SimulateAudioStops() {
content::WebContentsTester::For(contents_)->SetIsCurrentlyAudible(false);
helper_->OnAudioStateChanged(false);
}
void AdvanceTime(base::TimeDelta duration) {
task_runner_->FastForwardBy(duration);
}
void ExpectNeverAudible() {
EXPECT_FALSE(helper_->WasEverAudible());
EXPECT_FALSE(helper_->IsCurrentlyAudible());
EXPECT_FALSE(helper_->WasRecentlyAudible());
}
void ExpectCurrentlyAudible() {
EXPECT_TRUE(helper_->WasEverAudible());
EXPECT_TRUE(helper_->IsCurrentlyAudible());
EXPECT_TRUE(helper_->WasRecentlyAudible());
}
void ExpectRecentlyAudible() {
EXPECT_TRUE(helper_->WasEverAudible());
EXPECT_FALSE(helper_->IsCurrentlyAudible());
EXPECT_TRUE(helper_->WasRecentlyAudible());
}
void ExpectNotRecentlyAudible() {
EXPECT_TRUE(helper_->WasEverAudible());
EXPECT_FALSE(helper_->IsCurrentlyAudible());
EXPECT_FALSE(helper_->WasRecentlyAudible());
}
void ExpectRecentlyAudibleTransition(bool recently_audible) {
EXPECT_EQ(recently_audible, recently_audible_messages_.front());
recently_audible_messages_.pop_front();
}
void VerifyAndClearExpectations() {
EXPECT_TRUE(recently_audible_messages_.empty());
}
private:
void OnRecentlyAudibleCallback(bool recently_audible) {
recently_audible_messages_.push_back(recently_audible);
}
// Mock time environment.
scoped_refptr<base::TestMockTimeTaskRunner> task_runner_ =
base::MakeRefCounted<base::TestMockTimeTaskRunner>();
std::unique_ptr<base::TestMockTimeTaskRunner::ScopedContext> scoped_context_;
// Environment for creating WebContents.
std::unique_ptr<content::TestWebContentsFactory> test_web_contents_factory_;
content::TestBrowserThreadBundle thread_bundle_;
TestingProfile testing_profile_;
// A test WebContents and its associated helper.
content::WebContents* contents_;
RecentlyAudibleHelper* helper_;
std::unique_ptr<RecentlyAudibleHelper::Subscription> subscription_;
std::list<bool> recently_audible_messages_;
DISALLOW_COPY_AND_ASSIGN(RecentlyAudibleHelperTest);
};
TEST_F(RecentlyAudibleHelperTest, AllStateTransitions) {
// Initially nothing has ever been audible.
ExpectNeverAudible();
VerifyAndClearExpectations();
// Start audio and expect a transition.
SimulateAudioStarts();
ExpectRecentlyAudibleTransition(true);
ExpectCurrentlyAudible();
VerifyAndClearExpectations();
// Keep audio playing and don't expect any transitions.
AdvanceTime(base::TimeDelta::FromSeconds(30));
ExpectCurrentlyAudible();
VerifyAndClearExpectations();
// Stop audio, but don't expect a transition.
SimulateAudioStops();
ExpectRecentlyAudible();
VerifyAndClearExpectations();
// Advance time by half the timeout period. Still don't expect a transition.
AdvanceTime(RecentlyAudibleHelper::kRecentlyAudibleTimeout / 2);
ExpectRecentlyAudible();
VerifyAndClearExpectations();
// Start audio again. Still don't expect a transition.
SimulateAudioStarts();
ExpectCurrentlyAudible();
VerifyAndClearExpectations();
// Advance time and stop audio, not expecting a transition.
AdvanceTime(base::TimeDelta::FromSeconds(30));
SimulateAudioStops();
ExpectRecentlyAudible();
VerifyAndClearExpectations();
// Advance time by the timeout period and this time expect a transition to not
// recently audible.
AdvanceTime(RecentlyAudibleHelper::kRecentlyAudibleTimeout);
ExpectRecentlyAudibleTransition(false);
ExpectNotRecentlyAudible();
VerifyAndClearExpectations();
}
...@@ -3001,7 +3001,6 @@ test("unit_tests") { ...@@ -3001,7 +3001,6 @@ test("unit_tests") {
"../browser/themes/theme_service_unittest.cc", "../browser/themes/theme_service_unittest.cc",
"../browser/themes/theme_syncable_service_unittest.cc", "../browser/themes/theme_syncable_service_unittest.cc",
"../browser/translate/translate_manager_render_view_host_unittest.cc", "../browser/translate/translate_manager_render_view_host_unittest.cc",
"../browser/ui/webui/theme_source_unittest.cc",
# The autofill popup is implemented in mostly native code on Android. # The autofill popup is implemented in mostly native code on Android.
"../browser/ui/autofill/autofill_popup_controller_unittest.cc", "../browser/ui/autofill/autofill_popup_controller_unittest.cc",
...@@ -3025,6 +3024,7 @@ test("unit_tests") { ...@@ -3025,6 +3024,7 @@ test("unit_tests") {
"../browser/ui/page_info/permission_menu_model_unittest.cc", "../browser/ui/page_info/permission_menu_model_unittest.cc",
"../browser/ui/passwords/manage_passwords_bubble_model_unittest.cc", "../browser/ui/passwords/manage_passwords_bubble_model_unittest.cc",
"../browser/ui/passwords/password_dialog_controller_impl_unittest.cc", "../browser/ui/passwords/password_dialog_controller_impl_unittest.cc",
"../browser/ui/recently_audible_helper_unittest.cc",
"../browser/ui/search/ntp_user_data_logger_unittest.cc", "../browser/ui/search/ntp_user_data_logger_unittest.cc",
"../browser/ui/search/search_ipc_router_policy_unittest.cc", "../browser/ui/search/search_ipc_router_policy_unittest.cc",
"../browser/ui/search/search_ipc_router_unittest.cc", "../browser/ui/search/search_ipc_router_unittest.cc",
...@@ -3068,6 +3068,7 @@ test("unit_tests") { ...@@ -3068,6 +3068,7 @@ test("unit_tests") {
"../browser/ui/webui/signin/login_ui_service_unittest.cc", "../browser/ui/webui/signin/login_ui_service_unittest.cc",
"../browser/ui/webui/site_settings_helper_unittest.cc", "../browser/ui/webui/site_settings_helper_unittest.cc",
"../browser/ui/webui/sync_internals_message_handler_unittest.cc", "../browser/ui/webui/sync_internals_message_handler_unittest.cc",
"../browser/ui/webui/theme_source_unittest.cc",
"../browser/ui/webui/web_dialog_web_contents_delegate_unittest.cc", "../browser/ui/webui/web_dialog_web_contents_delegate_unittest.cc",
"../browser/ui/window_sizer/window_sizer_common_unittest.cc", "../browser/ui/window_sizer/window_sizer_common_unittest.cc",
"../browser/ui/window_sizer/window_sizer_common_unittest.h", "../browser/ui/window_sizer/window_sizer_common_unittest.h",
......
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