Media Engagement: use playback, tab visibility and tab muted as signals.

The implementation does not yet use the following signals:
- audio track;
- muted state;
- video size.

Furthermore, the information are not yet recorded.

Bug: 715051
Change-Id: I94176ab98b05d5251f0f580c39d064b670e0496d
Reviewed-on: https://chromium-review.googlesource.com/505618
Reviewed-by: Jennifer Apacible <[email protected]>
Commit-Queue: Mounir Lamouri <[email protected]>
Cr-Commit-Position: refs/heads/master@{#474228}
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 0a4d786f..9185306 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -573,6 +573,8 @@
     "media/media_access_handler.h",
     "media/media_device_id_salt.cc",
     "media/media_device_id_salt.h",
+    "media/media_engagement_contents_observer.cc",
+    "media/media_engagement_contents_observer.h",
     "media/media_engagement_service.cc",
     "media/media_engagement_service.h",
     "media/media_engagement_service_factory.cc",
diff --git a/chrome/browser/media/media_engagement_contents_observer.cc b/chrome/browser/media/media_engagement_contents_observer.cc
new file mode 100644
index 0000000..a595b5b
--- /dev/null
+++ b/chrome/browser/media/media_engagement_contents_observer.cc
@@ -0,0 +1,129 @@
+// Copyright 2017 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/media/media_engagement_contents_observer.h"
+
+#include "chrome/browser/media/media_engagement_service.h"
+#include "content/public/browser/navigation_handle.h"
+#include "content/public/browser/web_contents.h"
+
+constexpr base::TimeDelta
+    MediaEngagementContentsObserver::kSignificantMediaPlaybackTime;
+
+MediaEngagementContentsObserver::MediaEngagementContentsObserver(
+    content::WebContents* web_contents,
+    MediaEngagementService* service)
+    : WebContentsObserver(web_contents),
+      service_(service),
+      playback_timer_(new base::Timer(true, false)) {}
+
+MediaEngagementContentsObserver::~MediaEngagementContentsObserver() = default;
+
+void MediaEngagementContentsObserver::WebContentsDestroyed() {
+  playback_timer_->Stop();
+  service_->contents_observers_.erase(this);
+  delete this;
+}
+
+void MediaEngagementContentsObserver::DidFinishNavigation(
+    content::NavigationHandle* navigation_handle) {
+  if (!navigation_handle->IsInMainFrame() ||
+      !navigation_handle->HasCommitted() ||
+      navigation_handle->IsSameDocument() || navigation_handle->IsErrorPage()) {
+    return;
+  }
+
+  DCHECK(!playback_timer_->IsRunning());
+  DCHECK(significant_players_.empty());
+
+  url::Origin new_origin(navigation_handle->GetURL());
+  if (committed_origin_.IsSameOriginWith(new_origin))
+    return;
+
+  committed_origin_ = new_origin;
+  significant_playback_recorded_ = false;
+
+  if (committed_origin_.unique())
+    return;
+
+  // TODO(mlamouri): record the visit into content settings.
+}
+
+void MediaEngagementContentsObserver::WasShown() {
+  is_visible_ = true;
+  UpdateTimer();
+}
+
+void MediaEngagementContentsObserver::WasHidden() {
+  is_visible_ = false;
+  UpdateTimer();
+}
+
+void MediaEngagementContentsObserver::MediaStartedPlaying(
+    const MediaPlayerInfo& media_player_info,
+    const MediaPlayerId& media_player_id) {
+  // TODO(mlamouri): check if:
+  // - the playback has the minimum size requirements;
+  // - the playback has an audio track;
+  // - the playback isn't muted.
+  DCHECK(significant_players_.find(media_player_id) ==
+         significant_players_.end());
+  significant_players_.insert(media_player_id);
+  UpdateTimer();
+}
+
+void MediaEngagementContentsObserver::MediaStoppedPlaying(
+    const MediaPlayerInfo& media_player_info,
+    const MediaPlayerId& media_player_id) {
+  DCHECK(significant_players_.find(media_player_id) !=
+         significant_players_.end());
+  significant_players_.erase(media_player_id);
+  UpdateTimer();
+}
+
+void MediaEngagementContentsObserver::DidUpdateAudioMutingState(bool muted) {
+  UpdateTimer();
+}
+
+void MediaEngagementContentsObserver::OnSignificantMediaPlaybackTime() {
+  DCHECK(!significant_playback_recorded_);
+
+  significant_playback_recorded_ = true;
+
+  if (committed_origin_.unique())
+    return;
+
+  // TODO(mlamouri): record the playback into content settings.
+}
+
+bool MediaEngagementContentsObserver::AreConditionsMet() const {
+  if (significant_players_.empty() || !is_visible_)
+    return false;
+
+  return !web_contents()->IsAudioMuted();
+}
+
+void MediaEngagementContentsObserver::UpdateTimer() {
+  if (significant_playback_recorded_)
+    return;
+
+  if (AreConditionsMet()) {
+    if (playback_timer_->IsRunning())
+      return;
+    playback_timer_->Start(
+        FROM_HERE, kSignificantMediaPlaybackTime,
+        base::Bind(
+            &MediaEngagementContentsObserver::OnSignificantMediaPlaybackTime,
+            base::Unretained(this)));
+  } else {
+    if (!playback_timer_->IsRunning())
+      return;
+    playback_timer_->Stop();
+  }
+}
+
+void MediaEngagementContentsObserver::SetTimerForTest(
+    std::unique_ptr<base::Timer> timer) {
+  playback_timer_ = std::move(timer);
+}
diff --git a/chrome/browser/media/media_engagement_contents_observer.h b/chrome/browser/media/media_engagement_contents_observer.h
new file mode 100644
index 0000000..94a57f7
--- /dev/null
+++ b/chrome/browser/media/media_engagement_contents_observer.h
@@ -0,0 +1,66 @@
+// Copyright 2017 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_MEDIA_MEDIA_ENGAGEMENT_CONTENTS_OBSERVER_H_
+#define CHROME_BROWSER_MEDIA_MEDIA_ENGAGEMENT_CONTENTS_OBSERVER_H_
+
+#include "content/public/browser/web_contents_observer.h"
+
+class MediaEngagementContentsObserverTest;
+class MediaEngagementService;
+
+class MediaEngagementContentsObserver : public content::WebContentsObserver {
+ public:
+  ~MediaEngagementContentsObserver() override;
+
+  // WebContentsObserver implementation.
+  void WebContentsDestroyed() override;
+  void DidFinishNavigation(
+      content::NavigationHandle* navigation_handle) override;
+  void WasShown() override;
+  void WasHidden() override;
+  void MediaStartedPlaying(const MediaPlayerInfo& media_player_info,
+                           const MediaPlayerId& media_player_id) override;
+  void MediaStoppedPlaying(const MediaPlayerInfo& media_player_info,
+                           const MediaPlayerId& media_player_id) override;
+  void DidUpdateAudioMutingState(bool muted) override;
+
+ private:
+  // Only MediaEngagementService can create a MediaEngagementContentsObserver.
+  friend MediaEngagementService;
+  friend MediaEngagementContentsObserverTest;
+
+  static constexpr base::TimeDelta kSignificantMediaPlaybackTime =
+      base::TimeDelta::FromSeconds(7);
+
+  MediaEngagementContentsObserver(content::WebContents* web_contents,
+                                  MediaEngagementService* service);
+
+  void OnSignificantMediaPlaybackTime();
+  bool AreConditionsMet() const;
+  void UpdateTimer();
+
+  void SetTimerForTest(std::unique_ptr<base::Timer> timer);
+
+  // |this| is owned by |service_|.
+  MediaEngagementService* service_;
+
+  // Timer that will fire when the playback time reaches the minimum for
+  // significant media playback.
+  std::unique_ptr<base::Timer> playback_timer_;
+
+  // Set of active players that can produce a significant playback. In other
+  // words, whether this set is empty can be used to know if there is a
+  // significant playback.
+  std::set<MediaPlayerId> significant_players_;
+
+  bool is_visible_ = false;
+  bool significant_playback_recorded_ = false;
+
+  url::Origin committed_origin_;
+
+  DISALLOW_COPY_AND_ASSIGN(MediaEngagementContentsObserver);
+};
+
+#endif  // CHROME_BROWSER_MEDIA_MEDIA_ENGAGEMENT_CONTENTS_OBSERVER_H_
diff --git a/chrome/browser/media/media_engagement_contents_observer_unittest.cc b/chrome/browser/media/media_engagement_contents_observer_unittest.cc
new file mode 100644
index 0000000..2eaf676
--- /dev/null
+++ b/chrome/browser/media/media_engagement_contents_observer_unittest.cc
@@ -0,0 +1,171 @@
+// Copyright 2017 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/media/media_engagement_contents_observer.h"
+
+#include "base/test/scoped_feature_list.h"
+#include "base/timer/mock_timer.h"
+#include "chrome/browser/media/media_engagement_service.h"
+#include "chrome/browser/media/media_engagement_service_factory.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "chrome/test/base/testing_profile.h"
+#include "content/public/browser/web_contents.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+class MediaEngagementContentsObserverTest
+    : public ChromeRenderViewHostTestHarness {
+ public:
+  void SetUp() override {
+    scoped_feature_list_.InitFromCommandLine("media-engagement", std::string());
+
+    ChromeRenderViewHostTestHarness::SetUp();
+
+    MediaEngagementService* service = MediaEngagementService::Get(profile());
+    ASSERT_TRUE(service);
+    contents_observer_ =
+        new MediaEngagementContentsObserver(web_contents(), service);
+
+    playback_timer_ = new base::MockTimer(true, false);
+    contents_observer_->SetTimerForTest(base::WrapUnique(playback_timer_));
+  }
+
+  bool IsTimerRunning() const { return playback_timer_->IsRunning(); }
+
+  bool WasSignificantPlaybackRecorded() const {
+    return contents_observer_->significant_playback_recorded_;
+  }
+
+  size_t GetSignificantActivePlayersCount() const {
+    return contents_observer_->significant_players_.size();
+  }
+
+  void SimulatePlaybackStarted(int id) {
+    content::WebContentsObserver::MediaPlayerInfo player_info(true);
+    content::WebContentsObserver::MediaPlayerId player_id =
+        std::make_pair(nullptr /* RenderFrameHost */, id);
+    contents_observer_->MediaStartedPlaying(player_info, player_id);
+  }
+
+  void SimulatePlaybackStopped(int id) {
+    content::WebContentsObserver::MediaPlayerInfo player_info(true);
+    content::WebContentsObserver::MediaPlayerId player_id =
+        std::make_pair(nullptr /* RenderFrameHost */, id);
+    contents_observer_->MediaStoppedPlaying(player_info, player_id);
+  }
+
+  void SimulateIsVisible() { contents_observer_->WasShown(); }
+
+  void SimulateIsHidden() { contents_observer_->WasHidden(); }
+
+  bool AreConditionsMet() const {
+    return contents_observer_->AreConditionsMet();
+  }
+
+  void SimulateSignificantPlaybackRecorded() {
+    contents_observer_->significant_playback_recorded_ = true;
+  }
+
+  void SimulatePlaybackTimerFired() { playback_timer_->Fire(); }
+
+ private:
+  // contents_observer_ auto-destroys when WebContents is destroyed.
+  MediaEngagementContentsObserver* contents_observer_;
+
+  base::test::ScopedFeatureList scoped_feature_list_;
+
+  base::MockTimer* playback_timer_;
+};
+
+// TODO(mlamouri): test that visits are not recorded multiple times when a
+// same-origin navigation happens.
+
+TEST_F(MediaEngagementContentsObserverTest, SignificantActivePlayerCount) {
+  EXPECT_EQ(0u, GetSignificantActivePlayersCount());
+
+  SimulatePlaybackStarted(0);
+  EXPECT_EQ(1u, GetSignificantActivePlayersCount());
+
+  SimulatePlaybackStarted(1);
+  EXPECT_EQ(2u, GetSignificantActivePlayersCount());
+
+  SimulatePlaybackStarted(2);
+  EXPECT_EQ(3u, GetSignificantActivePlayersCount());
+
+  SimulatePlaybackStopped(1);
+  EXPECT_EQ(2u, GetSignificantActivePlayersCount());
+
+  SimulatePlaybackStopped(0);
+  EXPECT_EQ(1u, GetSignificantActivePlayersCount());
+
+  SimulatePlaybackStopped(2);
+  EXPECT_EQ(0u, GetSignificantActivePlayersCount());
+}
+
+TEST_F(MediaEngagementContentsObserverTest, AreConditionsMet) {
+  EXPECT_FALSE(AreConditionsMet());
+
+  SimulatePlaybackStarted(0);
+  SimulateIsVisible();
+  web_contents()->SetAudioMuted(false);
+  EXPECT_TRUE(AreConditionsMet());
+
+  web_contents()->SetAudioMuted(true);
+  EXPECT_FALSE(AreConditionsMet());
+
+  web_contents()->SetAudioMuted(false);
+  SimulateIsHidden();
+  EXPECT_FALSE(AreConditionsMet());
+
+  SimulateIsVisible();
+  SimulatePlaybackStopped(0);
+  EXPECT_FALSE(AreConditionsMet());
+
+  SimulatePlaybackStarted(0);
+  EXPECT_TRUE(AreConditionsMet());
+}
+
+TEST_F(MediaEngagementContentsObserverTest, TimerRunsDependingOnConditions) {
+  EXPECT_FALSE(IsTimerRunning());
+
+  SimulatePlaybackStarted(0);
+  SimulateIsVisible();
+  web_contents()->SetAudioMuted(false);
+  EXPECT_TRUE(IsTimerRunning());
+
+  web_contents()->SetAudioMuted(true);
+  EXPECT_FALSE(IsTimerRunning());
+
+  web_contents()->SetAudioMuted(false);
+  SimulateIsHidden();
+  EXPECT_FALSE(IsTimerRunning());
+
+  SimulateIsVisible();
+  SimulatePlaybackStopped(0);
+  EXPECT_FALSE(IsTimerRunning());
+
+  SimulatePlaybackStarted(0);
+  EXPECT_TRUE(IsTimerRunning());
+}
+
+TEST_F(MediaEngagementContentsObserverTest, TimerDoesNotRunIfEntryRecorded) {
+  SimulateSignificantPlaybackRecorded();
+
+  SimulatePlaybackStarted(0);
+  SimulateIsVisible();
+  web_contents()->SetAudioMuted(false);
+
+  EXPECT_FALSE(IsTimerRunning());
+}
+
+TEST_F(MediaEngagementContentsObserverTest,
+       SignificantPlaybackRecordedWhenTimerFires) {
+  SimulatePlaybackStarted(0);
+  SimulateIsVisible();
+  web_contents()->SetAudioMuted(false);
+  EXPECT_TRUE(IsTimerRunning());
+  EXPECT_FALSE(WasSignificantPlaybackRecorded());
+
+  SimulatePlaybackTimerFired();
+  EXPECT_TRUE(WasSignificantPlaybackRecorded());
+}
diff --git a/chrome/browser/media/media_engagement_service.cc b/chrome/browser/media/media_engagement_service.cc
index 2eb7ffb..1c4de26e 100644
--- a/chrome/browser/media/media_engagement_service.cc
+++ b/chrome/browser/media/media_engagement_service.cc
@@ -4,34 +4,12 @@
 
 #include "chrome/browser/media/media_engagement_service.h"
 
+#include "chrome/browser/media/media_engagement_contents_observer.h"
 #include "chrome/browser/media/media_engagement_service_factory.h"
 #include "chrome/browser/profiles/profile.h"
 #include "content/public/browser/web_contents.h"
-#include "content/public/browser/web_contents_observer.h"
 #include "media/base/media_switches.h"
 
-class MediaEngagementService::ContentsObserver
-    : public content::WebContentsObserver {
- public:
-  ContentsObserver(content::WebContents* web_contents,
-                   MediaEngagementService* service)
-      : WebContentsObserver(web_contents), service_(service) {}
-
-  ~ContentsObserver() override = default;
-
-  // WebContentsObserver implementation.
-  void WebContentsDestroyed() override {
-    service_->contents_observers_.erase(this);
-    delete this;
-  }
-
- private:
-  // |this| is owned by |service_|.
-  MediaEngagementService* service_;
-
-  DISALLOW_COPY_AND_ASSIGN(ContentsObserver);
-};
-
 // static
 bool MediaEngagementService::IsEnabled() {
   return base::FeatureList::IsEnabled(media::kMediaEngagement);
@@ -52,7 +30,7 @@
   if (!service)
     return;
   service->contents_observers_.insert(
-      new ContentsObserver(web_contents, service));
+      new MediaEngagementContentsObserver(web_contents, service));
 }
 
 MediaEngagementService::MediaEngagementService(Profile* profile) {
diff --git a/chrome/browser/media/media_engagement_service.h b/chrome/browser/media/media_engagement_service.h
index 3701c02..7f4d228a 100644
--- a/chrome/browser/media/media_engagement_service.h
+++ b/chrome/browser/media/media_engagement_service.h
@@ -10,6 +10,7 @@
 #include "base/macros.h"
 #include "components/keyed_service/core/keyed_service.h"
 
+class MediaEngagementContentsObserver;
 class Profile;
 
 namespace content {
@@ -32,9 +33,9 @@
   ~MediaEngagementService() override;
 
  private:
-  class ContentsObserver;
+  friend MediaEngagementContentsObserver;
 
-  std::set<ContentsObserver*> contents_observers_;
+  std::set<MediaEngagementContentsObserver*> contents_observers_;
 
   DISALLOW_COPY_AND_ASSIGN(MediaEngagementService);
 };
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 90fe4f7..8ec65cc 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -3092,6 +3092,7 @@
     "../browser/manifest/manifest_icon_selector_unittest.cc",
     "../browser/media/android/router/media_router_android_unittest.cc",
     "../browser/media/cast_remoting_connector_unittest.cc",
+    "../browser/media/media_engagement_contents_observer_unittest.cc",
     "../browser/media/midi_permission_context_unittest.cc",
     "../browser/media/router/browser_presentation_connection_proxy_unittest.cc",
     "../browser/media/router/create_presentation_connection_request_unittest.cc",