idle-detection: Block permission requests from incognito profiles
This change implements automatic blocking of requests for the
"idle-detection" permission from incognito profiles in order to prevent
the feature from being used to correlate incognito and non-incognito
sessions.
As for requests for the "notifications" permission rather than simply
automatically denying the request (which would be an incognito oracle)
this patch waits a random amount of time before triggering the denial.
The code from the NotificationsPermissionContext is cleaned up and
reused for this.
Bug: 878979
Change-Id: I57c57f39457932f570d7094882dc00d9c0eafa16
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2360943
Commit-Queue: Reilly Grant <[email protected]>
Reviewed-by: Scott Violet <[email protected]>
Reviewed-by: Balazs Engedy <[email protected]>
Cr-Commit-Position: refs/heads/master@{#807660}
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 9bec634..74c3e26 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -1806,6 +1806,8 @@
"usb/web_usb_service_impl.h",
"video_tutorials/video_tutorial_service_factory.cc",
"video_tutorials/video_tutorial_service_factory.h",
+ "visibility_timer_tab_helper.cc",
+ "visibility_timer_tab_helper.h",
"vr/ui_suppressed_element.h",
"vr/vr_tab_helper.cc",
"vr/vr_tab_helper.h",
diff --git a/chrome/browser/idle/idle_detection_permission_context.cc b/chrome/browser/idle/idle_detection_permission_context.cc
index 1042b71..258b92fe 100644
--- a/chrome/browser/idle/idle_detection_permission_context.cc
+++ b/chrome/browser/idle/idle_detection_permission_context.cc
@@ -4,8 +4,13 @@
#include "chrome/browser/idle/idle_detection_permission_context.h"
+#include "base/bind.h"
+#include "base/location.h"
+#include "base/rand_util.h"
+#include "chrome/browser/visibility_timer_tab_helper.h"
#include "components/content_settings/browser/page_specific_content_settings.h"
#include "components/permissions/permission_request_id.h"
+#include "content/public/browser/browser_context.h"
#include "url/gurl.h"
IdleDetectionPermissionContext::IdleDetectionPermissionContext(
@@ -35,3 +40,37 @@
bool IdleDetectionPermissionContext::IsRestrictedToSecureOrigins() const {
return true;
}
+
+void IdleDetectionPermissionContext::DecidePermission(
+ content::WebContents* web_contents,
+ const permissions::PermissionRequestID& id,
+ const GURL& requesting_origin,
+ const GURL& embedding_origin,
+ bool user_gesture,
+ permissions::BrowserPermissionCallback callback) {
+ // Idle detection permission is always denied in incognito. To prevent sites
+ // from using that to detect whether incognito mode is active, we deny after a
+ // random time delay, to simulate a user clicking a bubble/infobar. See also
+ // ContentSettingsRegistry::Init, which marks idle detection as
+ // INHERIT_IF_LESS_PERMISSIVE, and
+ // PermissionMenuModel::PermissionMenuModel which prevents users from manually
+ // allowing the permission.
+ if (browser_context()->IsOffTheRecord()) {
+ // Random number of seconds in the range [1.0, 2.0).
+ double delay_seconds = 1.0 + 1.0 * base::RandDouble();
+ VisibilityTimerTabHelper::CreateForWebContents(web_contents);
+ VisibilityTimerTabHelper::FromWebContents(web_contents)
+ ->PostTaskAfterVisibleDelay(
+ FROM_HERE,
+ base::BindOnce(&IdleDetectionPermissionContext::NotifyPermissionSet,
+ weak_factory_.GetWeakPtr(), id, requesting_origin,
+ embedding_origin, std::move(callback),
+ /*persist=*/true, CONTENT_SETTING_BLOCK),
+ base::TimeDelta::FromSecondsD(delay_seconds));
+ return;
+ }
+
+ PermissionContextBase::DecidePermission(web_contents, id, requesting_origin,
+ embedding_origin, user_gesture,
+ std::move(callback));
+}
diff --git a/chrome/browser/idle/idle_detection_permission_context.h b/chrome/browser/idle/idle_detection_permission_context.h
index 1265309e..a0c0ac5c 100644
--- a/chrome/browser/idle/idle_detection_permission_context.h
+++ b/chrome/browser/idle/idle_detection_permission_context.h
@@ -6,6 +6,7 @@
#define CHROME_BROWSER_IDLE_IDLE_DETECTION_PERMISSION_CONTEXT_H_
#include "base/macros.h"
+#include "base/memory/weak_ptr.h"
#include "components/permissions/permission_context_base.h"
class IdleDetectionPermissionContext
@@ -21,6 +22,15 @@
const GURL& requesting_frame,
bool allowed) override;
bool IsRestrictedToSecureOrigins() const override;
+ void DecidePermission(
+ content::WebContents* web_contents,
+ const permissions::PermissionRequestID& id,
+ const GURL& requesting_origin,
+ const GURL& embedding_origin,
+ bool user_gesture,
+ permissions::BrowserPermissionCallback callback) override;
+
+ base::WeakPtrFactory<IdleDetectionPermissionContext> weak_factory_{this};
DISALLOW_COPY_AND_ASSIGN(IdleDetectionPermissionContext);
};
diff --git a/chrome/browser/idle/idle_detection_permission_context_unittest.cc b/chrome/browser/idle/idle_detection_permission_context_unittest.cc
new file mode 100644
index 0000000..05ecde76
--- /dev/null
+++ b/chrome/browser/idle/idle_detection_permission_context_unittest.cc
@@ -0,0 +1,190 @@
+// Copyright 2020 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/idle/idle_detection_permission_context.h"
+
+#include "base/test/task_environment.h"
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "components/content_settings/core/browser/host_content_settings_map.h"
+#include "components/permissions/permission_request_id.h"
+#include "content/public/browser/render_process_host.h"
+#include "content/public/browser/web_contents.h"
+
+namespace {
+
+class TestIdleDetectionPermissionContext
+ : public IdleDetectionPermissionContext {
+ public:
+ explicit TestIdleDetectionPermissionContext(Profile* profile)
+ : IdleDetectionPermissionContext(profile),
+ permission_set_count_(0),
+ last_permission_set_persisted_(false),
+ last_permission_set_setting_(CONTENT_SETTING_DEFAULT) {}
+
+ int permission_set_count() const { return permission_set_count_; }
+ bool last_permission_set_persisted() const {
+ return last_permission_set_persisted_;
+ }
+ ContentSetting last_permission_set_setting() const {
+ return last_permission_set_setting_;
+ }
+
+ ContentSetting GetContentSettingFromMap(const GURL& url_a,
+ const GURL& url_b) {
+ return HostContentSettingsMapFactory::GetForProfile(browser_context())
+ ->GetContentSetting(url_a.GetOrigin(), url_b.GetOrigin(),
+ content_settings_type(), std::string());
+ }
+
+ private:
+ // IdleDetectionPermissionContext:
+ void NotifyPermissionSet(const permissions::PermissionRequestID& id,
+ const GURL& requesting_origin,
+ const GURL& embedder_origin,
+ permissions::BrowserPermissionCallback callback,
+ bool persist,
+ ContentSetting content_setting) override {
+ permission_set_count_++;
+ last_permission_set_persisted_ = persist;
+ last_permission_set_setting_ = content_setting;
+ IdleDetectionPermissionContext::NotifyPermissionSet(
+ id, requesting_origin, embedder_origin, std::move(callback), persist,
+ content_setting);
+ }
+
+ int permission_set_count_;
+ bool last_permission_set_persisted_;
+ ContentSetting last_permission_set_setting_;
+};
+
+} // namespace
+
+class IdleDetectionPermissionContextTest
+ : public ChromeRenderViewHostTestHarness {
+ public:
+ IdleDetectionPermissionContextTest()
+ : ChromeRenderViewHostTestHarness(
+ base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
+};
+
+// Tests auto-denial after a time delay in incognito.
+TEST_F(IdleDetectionPermissionContextTest, TestDenyInIncognitoAfterDelay) {
+ TestIdleDetectionPermissionContext permission_context(
+ profile()->GetPrimaryOTRProfile());
+ GURL url("https://www.example.com");
+ NavigateAndCommit(url);
+
+ const permissions::PermissionRequestID id(
+ web_contents()->GetMainFrame()->GetProcess()->GetID(),
+ web_contents()->GetMainFrame()->GetRoutingID(), -1);
+
+ ASSERT_EQ(0, permission_context.permission_set_count());
+ ASSERT_FALSE(permission_context.last_permission_set_persisted());
+ ASSERT_EQ(CONTENT_SETTING_DEFAULT,
+ permission_context.last_permission_set_setting());
+
+ permission_context.RequestPermission(
+ web_contents(), id, url, true /* user_gesture */, base::DoNothing());
+
+ // Should be blocked after 1-2 seconds, but the timer is reset whenever the
+ // tab is not visible, so these 500ms never add up to >= 1 second.
+ for (int n = 0; n < 10; n++) {
+ web_contents()->WasShown();
+ task_environment()->FastForwardBy(base::TimeDelta::FromMilliseconds(500));
+ web_contents()->WasHidden();
+ }
+
+ EXPECT_EQ(0, permission_context.permission_set_count());
+ EXPECT_EQ(CONTENT_SETTING_ASK,
+ permission_context.GetContentSettingFromMap(url, url));
+
+ // Time elapsed whilst hidden is not counted.
+ // n.b. This line also clears out any old scheduled timer tasks. This is
+ // important, because otherwise Timer::Reset (triggered by
+ // VisibilityTimerTabHelper::WasShown) may choose to re-use an existing
+ // scheduled task, and when it fires Timer::RunScheduledTask will call
+ // TimeTicks::Now() (which unlike task_environment()->NowTicks(), we can't
+ // fake), and miscalculate the remaining delay at which to fire the timer.
+ task_environment()->FastForwardBy(base::TimeDelta::FromDays(1));
+
+ EXPECT_EQ(0, permission_context.permission_set_count());
+ EXPECT_EQ(CONTENT_SETTING_ASK,
+ permission_context.GetContentSettingFromMap(url, url));
+
+ // Should be blocked after 1-2 seconds. So 500ms is not enough.
+ web_contents()->WasShown();
+ task_environment()->FastForwardBy(base::TimeDelta::FromMilliseconds(500));
+
+ EXPECT_EQ(0, permission_context.permission_set_count());
+ EXPECT_EQ(CONTENT_SETTING_ASK,
+ permission_context.GetContentSettingFromMap(url, url));
+
+ // But 5*500ms > 2 seconds, so it should now be blocked.
+ for (int n = 0; n < 4; n++)
+ task_environment()->FastForwardBy(base::TimeDelta::FromMilliseconds(500));
+
+ EXPECT_EQ(1, permission_context.permission_set_count());
+ EXPECT_TRUE(permission_context.last_permission_set_persisted());
+ EXPECT_EQ(CONTENT_SETTING_BLOCK,
+ permission_context.last_permission_set_setting());
+ EXPECT_EQ(CONTENT_SETTING_BLOCK,
+ permission_context.GetContentSettingFromMap(url, url));
+}
+
+// Tests how multiple parallel permission requests get auto-denied in incognito.
+TEST_F(IdleDetectionPermissionContextTest, TestParallelDenyInIncognito) {
+ TestIdleDetectionPermissionContext permission_context(
+ profile()->GetPrimaryOTRProfile());
+ GURL url("https://www.example.com");
+ NavigateAndCommit(url);
+ web_contents()->WasShown();
+
+ const permissions::PermissionRequestID id0(
+ web_contents()->GetMainFrame()->GetProcess()->GetID(),
+ web_contents()->GetMainFrame()->GetRoutingID(), 0);
+ const permissions::PermissionRequestID id1(
+ web_contents()->GetMainFrame()->GetProcess()->GetID(),
+ web_contents()->GetMainFrame()->GetRoutingID(), 1);
+
+ ASSERT_EQ(0, permission_context.permission_set_count());
+ ASSERT_FALSE(permission_context.last_permission_set_persisted());
+ ASSERT_EQ(CONTENT_SETTING_DEFAULT,
+ permission_context.last_permission_set_setting());
+
+ permission_context.RequestPermission(
+ web_contents(), id0, url, /*user_gesture=*/true, base::DoNothing());
+ permission_context.RequestPermission(
+ web_contents(), id1, url, /*user_gesture=*/true, base::DoNothing());
+
+ EXPECT_EQ(0, permission_context.permission_set_count());
+ EXPECT_EQ(CONTENT_SETTING_ASK,
+ permission_context.GetContentSettingFromMap(url, url));
+
+ // Fast forward up to 2.5 seconds. Stop as soon as the first permission
+ // request is auto-denied.
+ for (int n = 0; n < 5; n++) {
+ task_environment()->FastForwardBy(base::TimeDelta::FromMilliseconds(500));
+ if (permission_context.permission_set_count())
+ break;
+ }
+
+ // Only the first permission request receives a response (crbug.com/577336).
+ EXPECT_EQ(1, permission_context.permission_set_count());
+ EXPECT_TRUE(permission_context.last_permission_set_persisted());
+ EXPECT_EQ(CONTENT_SETTING_BLOCK,
+ permission_context.last_permission_set_setting());
+ EXPECT_EQ(CONTENT_SETTING_BLOCK,
+ permission_context.GetContentSettingFromMap(url, url));
+
+ // After another 2.5 seconds, the second permission request should also have
+ // received a response.
+ task_environment()->FastForwardBy(base::TimeDelta::FromMilliseconds(2500));
+ EXPECT_EQ(2, permission_context.permission_set_count());
+ EXPECT_TRUE(permission_context.last_permission_set_persisted());
+ EXPECT_EQ(CONTENT_SETTING_BLOCK,
+ permission_context.last_permission_set_setting());
+ EXPECT_EQ(CONTENT_SETTING_BLOCK,
+ permission_context.GetContentSettingFromMap(url, url));
+}
diff --git a/chrome/browser/notifications/notification_permission_context.cc b/chrome/browser/notifications/notification_permission_context.cc
index 9f0c710d..642d937 100644
--- a/chrome/browser/notifications/notification_permission_context.cc
+++ b/chrome/browser/notifications/notification_permission_context.cc
@@ -7,24 +7,19 @@
#include "base/bind.h"
#include "base/callback.h"
#include "base/callback_helpers.h"
-#include "base/containers/circular_deque.h"
#include "base/location.h"
#include "base/rand_util.h"
#include "base/single_thread_task_runner.h"
#include "base/stl_util.h"
#include "base/threading/thread_task_runner_handle.h"
-#include "base/timer/timer.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/visibility_timer_tab_helper.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings_pattern.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/permissions/permission_request_id.h"
#include "content/public/browser/browser_thread.h"
-#include "content/public/browser/render_frame_host.h"
-#include "content/public/browser/web_contents_observer.h"
-#include "content/public/browser/web_contents_user_data.h"
-#include "content/public/common/page_visibility_state.h"
#include "url/gurl.h"
#if BUILDFLAG(ENABLE_EXTENSIONS)
@@ -38,146 +33,6 @@
#include "ui/message_center/public/cpp/notifier_id.h"
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
-namespace {
-
-// At most one of these is attached to each WebContents. It allows posting
-// delayed tasks whose timer only counts down whilst the WebContents is visible
-// (and whose timer is reset whenever the WebContents stops being visible).
-class VisibilityTimerTabHelper
- : public content::WebContentsObserver,
- public content::WebContentsUserData<VisibilityTimerTabHelper> {
- public:
- VisibilityTimerTabHelper(const VisibilityTimerTabHelper&) = delete;
- VisibilityTimerTabHelper& operator=(const VisibilityTimerTabHelper&) = delete;
- ~VisibilityTimerTabHelper() override {}
-
- // Runs |task| after the WebContents has been visible for a consecutive
- // duration of at least |visible_delay|.
- void PostTaskAfterVisibleDelay(const base::Location& from_here,
- base::OnceClosure task,
- base::TimeDelta visible_delay,
- const permissions::PermissionRequestID& id);
-
- // Deletes any earlier task(s) that match |id|.
- void CancelTask(const permissions::PermissionRequestID& id);
-
- // WebContentsObserver:
- void OnVisibilityChanged(content::Visibility visibility) override;
- void WebContentsDestroyed() override;
-
- private:
- friend class content::WebContentsUserData<VisibilityTimerTabHelper>;
- explicit VisibilityTimerTabHelper(content::WebContents* contents);
-
- void RunTask(base::OnceClosure task);
-
- bool is_visible_;
-
- struct Task {
- Task(const permissions::PermissionRequestID& id,
- std::unique_ptr<base::RetainingOneShotTimer> timer)
- : id(id), timer(std::move(timer)) {}
-
- // Move-only.
- Task(Task&&) noexcept = default;
- Task(const Task&) = delete;
-
- Task& operator=(Task&& other) {
- id = other.id;
- timer = std::move(other.timer);
- return *this;
- }
-
- permissions::PermissionRequestID id;
- std::unique_ptr<base::RetainingOneShotTimer> timer;
- };
- base::circular_deque<Task> task_queue_;
-
- WEB_CONTENTS_USER_DATA_KEY_DECL();
-};
-
-WEB_CONTENTS_USER_DATA_KEY_IMPL(VisibilityTimerTabHelper)
-
-VisibilityTimerTabHelper::VisibilityTimerTabHelper(
- content::WebContents* contents)
- : content::WebContentsObserver(contents) {
- if (!contents->GetMainFrame()) {
- is_visible_ = false;
- } else {
- switch (contents->GetMainFrame()->GetVisibilityState()) {
- case content::PageVisibilityState::kHidden:
- case content::PageVisibilityState::kHiddenButPainting:
- is_visible_ = false;
- break;
- case content::PageVisibilityState::kVisible:
- is_visible_ = true;
- break;
- }
- }
-}
-
-void VisibilityTimerTabHelper::PostTaskAfterVisibleDelay(
- const base::Location& from_here,
- base::OnceClosure task,
- base::TimeDelta visible_delay,
- const permissions::PermissionRequestID& id) {
- if (web_contents()->IsBeingDestroyed())
- return;
-
- // Safe to use Unretained, as destroying |this| will destroy task_queue_,
- // hence cancelling all timers.
- // RetainingOneShotTimer is used which needs a RepeatingCallback, but we
- // only have it run this callback a single time, and destroy it after.
- auto timer = std::make_unique<base::RetainingOneShotTimer>(
- from_here, visible_delay,
- base::AdaptCallbackForRepeating(
- base::BindOnce(&VisibilityTimerTabHelper::RunTask,
- base::Unretained(this), std::move(task))));
- DCHECK(!timer->IsRunning());
-
- task_queue_.emplace_back(id, std::move(timer));
-
- if (is_visible_ && task_queue_.size() == 1)
- task_queue_.front().timer->Reset();
-}
-
-void VisibilityTimerTabHelper::CancelTask(
- const permissions::PermissionRequestID& id) {
- bool deleting_front = task_queue_.front().id == id;
-
- base::EraseIf(task_queue_, [id](const Task& task) { return task.id == id; });
-
- if (!task_queue_.empty() && is_visible_ && deleting_front)
- task_queue_.front().timer->Reset();
-}
-
-void VisibilityTimerTabHelper::OnVisibilityChanged(
- content::Visibility visibility) {
- if (visibility == content::Visibility::VISIBLE) {
- if (!is_visible_ && !task_queue_.empty())
- task_queue_.front().timer->Reset();
- is_visible_ = true;
- } else {
- if (is_visible_ && !task_queue_.empty())
- task_queue_.front().timer->Stop();
- is_visible_ = false;
- }
-}
-
-void VisibilityTimerTabHelper::WebContentsDestroyed() {
- task_queue_.clear();
-}
-
-void VisibilityTimerTabHelper::RunTask(base::OnceClosure task) {
- DCHECK(is_visible_);
- std::move(task).Run();
- task_queue_.pop_front();
- if (!task_queue_.empty())
- task_queue_.front().timer->Reset();
-}
-
-} // namespace
-
// static
void NotificationPermissionContext::UpdatePermission(
content::BrowserContext* browser_context,
@@ -300,7 +155,7 @@
requesting_origin, embedding_origin,
std::move(callback), true /* persist */,
CONTENT_SETTING_BLOCK),
- base::TimeDelta::FromSecondsD(delay_seconds), id);
+ base::TimeDelta::FromSecondsD(delay_seconds));
return;
}
diff --git a/chrome/browser/ui/page_info/permission_menu_model.cc b/chrome/browser/ui/page_info/permission_menu_model.cc
index b95c7fe..53528b2 100644
--- a/chrome/browser/ui/page_info/permission_menu_model.cc
+++ b/chrome/browser/ui/page_info/permission_menu_model.cc
@@ -79,8 +79,10 @@
bool PermissionMenuModel::ShouldShowAllow(const GURL& url) {
switch (permission_.type) {
- // Notifications does not support CONTENT_SETTING_ALLOW in incognito.
+ // Notifications and idle detection do not support CONTENT_SETTING_ALLOW in
+ // incognito.
case ContentSettingsType::NOTIFICATIONS:
+ case ContentSettingsType::IDLE_DETECTION:
return !permission_.is_incognito;
// Media only supports CONTENT_SETTING_ALLOW for secure origins.
case ContentSettingsType::MEDIASTREAM_MIC:
diff --git a/chrome/browser/visibility_timer_tab_helper.cc b/chrome/browser/visibility_timer_tab_helper.cc
new file mode 100644
index 0000000..fe9be91
--- /dev/null
+++ b/chrome/browser/visibility_timer_tab_helper.cc
@@ -0,0 +1,65 @@
+// Copyright 2020 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/visibility_timer_tab_helper.h"
+
+#include <utility>
+
+#include "base/callback_helpers.h"
+#include "base/logging.h"
+#include "base/timer/timer.h"
+#include "content/public/browser/visibility.h"
+#include "content/public/browser/web_contents.h"
+
+WEB_CONTENTS_USER_DATA_KEY_IMPL(VisibilityTimerTabHelper)
+
+VisibilityTimerTabHelper::~VisibilityTimerTabHelper() = default;
+
+void VisibilityTimerTabHelper::PostTaskAfterVisibleDelay(
+ const base::Location& from_here,
+ base::OnceClosure task,
+ base::TimeDelta visible_delay) {
+ if (web_contents()->IsBeingDestroyed())
+ return;
+
+ // Safe to use Unretained, as destroying |this| will destroy task_queue_,
+ // hence cancelling all timers.
+ // RetainingOneShotTimer is used which needs a RepeatingCallback, but we
+ // only have it run this callback a single time, and destroy it after.
+ task_queue_.push_back(std::make_unique<base::RetainingOneShotTimer>(
+ from_here, visible_delay,
+ base::AdaptCallbackForRepeating(
+ base::BindOnce(&VisibilityTimerTabHelper::RunTask,
+ base::Unretained(this), std::move(task)))));
+ DCHECK(!task_queue_.back()->IsRunning());
+
+ if (web_contents()->GetVisibility() == content::Visibility::VISIBLE &&
+ task_queue_.size() == 1) {
+ task_queue_.front()->Reset();
+ }
+}
+
+void VisibilityTimerTabHelper::OnVisibilityChanged(
+ content::Visibility visibility) {
+ if (!task_queue_.empty()) {
+ if (visibility == content::Visibility::VISIBLE)
+ task_queue_.front()->Reset();
+ else
+ task_queue_.front()->Stop();
+ }
+}
+
+VisibilityTimerTabHelper::VisibilityTimerTabHelper(
+ content::WebContents* contents)
+ : content::WebContentsObserver(contents) {}
+
+void VisibilityTimerTabHelper::RunTask(base::OnceClosure task) {
+ DCHECK_EQ(web_contents()->GetVisibility(), content::Visibility::VISIBLE);
+
+ task_queue_.pop_front();
+ if (!task_queue_.empty())
+ task_queue_.front()->Reset();
+
+ std::move(task).Run();
+}
diff --git a/chrome/browser/visibility_timer_tab_helper.h b/chrome/browser/visibility_timer_tab_helper.h
new file mode 100644
index 0000000..28ffbae
--- /dev/null
+++ b/chrome/browser/visibility_timer_tab_helper.h
@@ -0,0 +1,52 @@
+// Copyright 2020 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_VISIBILITY_TIMER_TAB_HELPER_H_
+#define CHROME_BROWSER_VISIBILITY_TIMER_TAB_HELPER_H_
+
+#include <memory>
+
+#include "base/containers/circular_deque.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "content/public/browser/web_contents_user_data.h"
+
+namespace base {
+class RetainingOneShotTimer;
+}
+
+// At most one of these is attached to each WebContents. It allows posting
+// delayed tasks whose timer only counts down whilst the WebContents is visible
+// (and whose timer is reset whenever the WebContents stops being visible).
+class VisibilityTimerTabHelper
+ : public content::WebContentsObserver,
+ public content::WebContentsUserData<VisibilityTimerTabHelper> {
+ public:
+ VisibilityTimerTabHelper(const VisibilityTimerTabHelper&&) = delete;
+ VisibilityTimerTabHelper& operator=(const VisibilityTimerTabHelper&&) =
+ delete;
+
+ ~VisibilityTimerTabHelper() override;
+
+ // Runs |task| after the WebContents has been visible for a consecutive
+ // duration of at least |visible_delay|.
+ void PostTaskAfterVisibleDelay(const base::Location& from_here,
+ base::OnceClosure task,
+ base::TimeDelta visible_delay);
+
+ // WebContentsObserver:
+ void OnVisibilityChanged(content::Visibility visibility) override;
+
+ private:
+ friend class content::WebContentsUserData<VisibilityTimerTabHelper>;
+ explicit VisibilityTimerTabHelper(content::WebContents* contents);
+
+ void RunTask(base::OnceClosure task);
+
+ base::circular_deque<std::unique_ptr<base::RetainingOneShotTimer>>
+ task_queue_;
+
+ WEB_CONTENTS_USER_DATA_KEY_DECL();
+};
+
+#endif // CHROME_BROWSER_VISIBILITY_TIMER_TAB_HELPER_H_
diff --git a/chrome/browser/visibility_timer_tab_helper_unittest.cc b/chrome/browser/visibility_timer_tab_helper_unittest.cc
new file mode 100644
index 0000000..ba5aecb
--- /dev/null
+++ b/chrome/browser/visibility_timer_tab_helper_unittest.cc
@@ -0,0 +1,64 @@
+// Copyright 2020 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/visibility_timer_tab_helper.h"
+
+#include "base/test/bind_test_util.h"
+#include "base/test/task_environment.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "content/public/browser/web_contents.h"
+
+class VisibilityTimerTabHelperTest : public ChromeRenderViewHostTestHarness {
+ public:
+ VisibilityTimerTabHelperTest()
+ : ChromeRenderViewHostTestHarness(
+ base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
+};
+
+TEST_F(VisibilityTimerTabHelperTest, Delay) {
+ bool task_executed = false;
+ VisibilityTimerTabHelper::CreateForWebContents(web_contents());
+ VisibilityTimerTabHelper::FromWebContents(web_contents())
+ ->PostTaskAfterVisibleDelay(FROM_HERE,
+ base::BindLambdaForTesting([&task_executed] {
+ EXPECT_FALSE(task_executed);
+ task_executed = true;
+ }),
+ base::TimeDelta::FromSecondsD(1));
+
+ EXPECT_FALSE(task_executed);
+
+ // The task will be executed after 1 second, but the timer is reset whenever
+ // the tab is not visible, so these 500ms never add up to >= 1 second.
+ for (int n = 0; n < 10; n++) {
+ web_contents()->WasShown();
+ task_environment()->FastForwardBy(base::TimeDelta::FromMilliseconds(500));
+ web_contents()->WasHidden();
+ }
+
+ EXPECT_FALSE(task_executed);
+
+ // Time elapsed whilst hidden is not counted.
+ // n.b. This line also clears out any old scheduled timer tasks. This is
+ // important, because otherwise Timer::Reset (triggered by
+ // VisibilityTimerTabHelper::WasShown) may choose to re-use an existing
+ // scheduled task, and when it fires Timer::RunScheduledTask will call
+ // TimeTicks::Now() (which unlike task_environment()->NowTicks(), we can't
+ // fake), and miscalculate the remaining delay at which to fire the timer.
+ task_environment()->FastForwardBy(base::TimeDelta::FromDays(1));
+
+ EXPECT_FALSE(task_executed);
+
+ // So 500ms is still not enough.
+ web_contents()->WasShown();
+ task_environment()->FastForwardBy(base::TimeDelta::FromMilliseconds(500));
+
+ EXPECT_FALSE(task_executed);
+
+ // But 5*500ms > 1 second, so it should now be blocked.
+ for (int n = 0; n < 4; n++)
+ task_environment()->FastForwardBy(base::TimeDelta::FromMilliseconds(500));
+
+ EXPECT_TRUE(task_executed);
+}
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index f497bedc..8588170 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -3376,6 +3376,7 @@
"../browser/history/android/visit_sql_handler_unittest.cc",
"../browser/history/domain_diversity_reporter_unittest.cc",
"../browser/history/history_tab_helper_unittest.cc",
+ "../browser/idle/idle_detection_permission_context_unittest.cc",
"../browser/infobars/mock_infobar_service.cc",
"../browser/infobars/mock_infobar_service.h",
"../browser/install_verification/win/module_info_unittest.cc",
@@ -3715,6 +3716,7 @@
"../browser/ui/webui/local_state/local_state_ui_unittest.cc",
"../browser/ui/webui/log_web_ui_url_unittest.cc",
"../browser/update_client/chrome_update_query_params_delegate_unittest.cc",
+ "../browser/visibility_timer_tab_helper_unittest.cc",
"../browser/vr/vr_tab_helper_unittest.cc",
"../browser/wake_lock/wake_lock_permission_context_unittest.cc",
"../browser/win/chrome_elf_init_unittest.cc",