Commit 012876ae authored by Collin Baker's avatar Collin Baker Committed by Commit Bot

Implement active tab tracker for reopen closed tab IPH

The triggering conditions for our in-product help include closing a
tab that has been active for at least some length of time. This adds a
class that tracks when tabs are activated and notifies its client when
an active tab is closed.

Bug: 887991
Change-Id: I7b3ca7a8f069d24c1751df9e301854ca99b2ff17
Reviewed-on: https://chromium-review.googlesource.com/c/1277819
Commit-Queue: Collin Baker <collinbaker@chromium.org>
Reviewed-by: default avatarPeter Boström <pbos@chromium.org>
Reviewed-by: default avatarBret Sepulveda <bsep@chromium.org>
Cr-Commit-Position: refs/heads/master@{#599811}
parent a754a47c
......@@ -3326,6 +3326,8 @@ jumbo_split_static_library("ui") {
if (enable_desktop_in_product_help) {
sources += [
"in_product_help/active_tab_tracker.cc",
"in_product_help/active_tab_tracker.h",
"in_product_help/reopen_tab_iph_trigger.cc",
"in_product_help/reopen_tab_iph_trigger.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/in_product_help/active_tab_tracker.h"
#include <utility>
#include "base/time/tick_clock.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
namespace in_product_help {
ActiveTabTracker::ActiveTabTracker(const base::TickClock* clock,
ActiveTabClosedCallback callback)
: clock_(clock), active_tab_closed_callback_(std::move(callback)) {
DCHECK(active_tab_closed_callback_);
}
ActiveTabTracker::~ActiveTabTracker() {
// All tab strip models should have been removed before destruction.
DCHECK(active_tab_changed_times_.empty());
}
void ActiveTabTracker::AddTabStripModel(TabStripModel* tab_strip_model) {
active_tab_changed_times_[tab_strip_model] = clock_->NowTicks();
tab_strip_model->AddObserver(this);
}
void ActiveTabTracker::RemoveTabStripModel(TabStripModel* tab_strip_model) {
// Get |std::map| iterator in |active_tab_changed_times_|.
auto it = active_tab_changed_times_.find(tab_strip_model);
DCHECK(it != active_tab_changed_times_.end());
// Stop observing and remove map element.
tab_strip_model->RemoveObserver(this);
active_tab_changed_times_.erase(it);
}
void ActiveTabTracker::OnTabStripModelChanged(
TabStripModel* model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) {
DCHECK(active_tab_changed_times_.find(model) !=
active_tab_changed_times_.end());
const int prev_active_tab_index = selection.old_model.active();
if (change.type() == TabStripModelChange::Type::kRemoved) {
for (const auto& delta : change.deltas()) {
// Ignore if the tab isn't being closed (this would happen if it were
// dragged to a different tab strip).
if (!delta.remove.will_be_deleted)
continue;
// If the closing tab was the active tab, call the callback.
if (delta.remove.index == prev_active_tab_index)
active_tab_closed_callback_.Run(
model, clock_->NowTicks() - active_tab_changed_times_[model]);
}
}
if (selection.active_tab_changed())
active_tab_changed_times_[model] = clock_->NowTicks();
}
} // namespace in_product_help
// 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_IN_PRODUCT_HELP_ACTIVE_TAB_TRACKER_H_
#define CHROME_BROWSER_UI_IN_PRODUCT_HELP_ACTIVE_TAB_TRACKER_H_
#include <unordered_map>
#include "base/callback.h"
#include "base/time/time.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
namespace base {
class TickClock;
}
class TabStripModel;
namespace in_product_help {
// Tracks when tabs become active and notifies client when active tabs are
// closed. The client must register and unregister |TabStripModel|s and set a
// callback to be called when an active tab is closed.
class ActiveTabTracker : public TabStripModelObserver {
public:
// Callback to be called when an active tab is closed. The callback takes two
// arguments: the TabStripModel that changed and how long the tab was active.
using ActiveTabClosedCallback =
base::RepeatingCallback<void(TabStripModel*, base::TimeDelta)>;
ActiveTabTracker(const base::TickClock* clock,
ActiveTabClosedCallback callback);
~ActiveTabTracker() override;
// Observes |tab_strip_model|. Its last activation time is set to the current
// time.
void AddTabStripModel(TabStripModel* tab_strip_model);
// Stops observing |tab_strip_model|. The |tab_strip_model| must have been
// previously added.
void RemoveTabStripModel(TabStripModel* tab_strip_model);
private:
// TabStripModelObserver:
void OnTabStripModelChanged(
TabStripModel* model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) override;
const base::TickClock* const clock_;
const ActiveTabClosedCallback active_tab_closed_callback_;
// Map containing the latest time the active tab changed for each tab strip
// model. Also serves as the list of all registered |TabStripModel|s.
std::unordered_map<TabStripModel*, base::TimeTicks> active_tab_changed_times_;
DISALLOW_COPY_AND_ASSIGN(ActiveTabTracker);
};
} // namespace in_product_help
#endif // CHROME_BROWSER_UI_IN_PRODUCT_HELP_ACTIVE_TAB_TRACKER_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/in_product_help/active_tab_tracker.h"
#include <memory>
#include <utility>
#include "base/test/mock_callback.h"
#include "base/test/simple_test_tick_clock.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/test_tab_strip_model_delegate.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/models/list_selection_model.h"
using ::testing::_;
using ::testing::InSequence;
using ::testing::Mock;
namespace {
class TestTabStripModelDelegateNoUnloadListener
: public TestTabStripModelDelegate {
public:
TestTabStripModelDelegateNoUnloadListener() = default;
~TestTabStripModelDelegateNoUnloadListener() override = default;
bool RunUnloadListenerBeforeClosing(content::WebContents* contents) override {
return false;
}
};
constexpr base::TimeDelta kTimeStep = base::TimeDelta::FromSeconds(1);
} // namespace
namespace in_product_help {
class ActiveTabTrackerTest : public ::testing::Test {
protected:
void SetUp() override {
TestingProfile::Builder profile_builder;
profile_ = profile_builder.Build();
}
void AddTab(TabStripModel* model) {
std::unique_ptr<content::WebContents> contents =
content::WebContents::Create(
content::WebContents::CreateParams(profile_.get()));
model->AppendWebContents(std::move(contents), true);
}
void CloseTabAt(TabStripModel* model, int index) {
model->CloseWebContentsAt(index,
TabStripModel::CloseTypes::CLOSE_USER_GESTURE);
}
Profile* profile() { return profile_.get(); }
base::SimpleTestTickClock* clock() { return &clock_; }
private:
// A |TestBrowserThreadBundle| is needed for creating and using
// |WebContents|es in a unit test.
content::TestBrowserThreadBundle thread_bundle_;
std::unique_ptr<TestingProfile> profile_;
base::SimpleTestTickClock clock_;
};
TEST_F(ActiveTabTrackerTest, NotifiesOnActiveTabClosed) {
TestTabStripModelDelegateNoUnloadListener delegate;
TabStripModel model(&delegate, profile());
base::MockCallback<ActiveTabTracker::ActiveTabClosedCallback> cb;
ActiveTabTracker tracker(clock(), cb.Get());
tracker.AddTabStripModel(&model);
AddTab(&model);
AddTab(&model);
clock()->Advance(kTimeStep);
model.ActivateTabAt(0, true);
clock()->Advance(kTimeStep);
EXPECT_CALL(cb, Run(&model, kTimeStep)).Times(1);
CloseTabAt(&model, 0);
tracker.RemoveTabStripModel(&model);
model.DetachWebContentsAt(0);
}
TEST_F(ActiveTabTrackerTest, UpdatesTimes) {
TestTabStripModelDelegateNoUnloadListener delegate;
TabStripModel model(&delegate, profile());
base::MockCallback<ActiveTabTracker::ActiveTabClosedCallback> cb;
ActiveTabTracker tracker(clock(), cb.Get());
tracker.AddTabStripModel(&model);
AddTab(&model);
AddTab(&model);
model.ActivateTabAt(0, true);
clock()->Advance(kTimeStep);
model.ActivateTabAt(1, true);
model.ActivateTabAt(0, true);
EXPECT_CALL(cb, Run(&model, base::TimeDelta())).Times(1);
CloseTabAt(&model, 0);
tracker.RemoveTabStripModel(&model);
model.DetachWebContentsAt(0);
}
TEST_F(ActiveTabTrackerTest, IgnoresInactiveTabs) {
TestTabStripModelDelegateNoUnloadListener delegate;
TabStripModel model(&delegate, profile());
base::MockCallback<ActiveTabTracker::ActiveTabClosedCallback> cb;
ActiveTabTracker tracker(clock(), cb.Get());
tracker.AddTabStripModel(&model);
AddTab(&model);
AddTab(&model);
model.ActivateTabAt(0, true);
EXPECT_CALL(cb, Run(_, _)).Times(0);
CloseTabAt(&model, 1);
tracker.RemoveTabStripModel(&model);
model.DetachWebContentsAt(0);
}
TEST_F(ActiveTabTrackerTest, TracksMultipleTabStripModels) {
TestTabStripModelDelegateNoUnloadListener delegate;
TabStripModel model_1(&delegate, profile());
TabStripModel model_2(&delegate, profile());
base::MockCallback<ActiveTabTracker::ActiveTabClosedCallback> cb;
ActiveTabTracker tracker(clock(), cb.Get());
tracker.AddTabStripModel(&model_1);
tracker.AddTabStripModel(&model_2);
AddTab(&model_1);
AddTab(&model_1);
AddTab(&model_2);
AddTab(&model_2);
clock()->Advance(kTimeStep);
model_1.ActivateTabAt(0, true);
clock()->Advance(kTimeStep);
model_2.ActivateTabAt(0, true);
{
InSequence seq;
EXPECT_CALL(cb, Run(&model_1, kTimeStep)).Times(1);
EXPECT_CALL(cb, Run(&model_2, base::TimeDelta())).Times(1);
}
CloseTabAt(&model_1, 0);
CloseTabAt(&model_2, 0);
tracker.RemoveTabStripModel(&model_1);
tracker.RemoveTabStripModel(&model_2);
model_1.DetachWebContentsAt(0);
model_2.DetachWebContentsAt(0);
}
TEST_F(ActiveTabTrackerTest, StopsObservingUponRemove) {
TestTabStripModelDelegateNoUnloadListener delegate;
TabStripModel model(&delegate, profile());
base::MockCallback<ActiveTabTracker::ActiveTabClosedCallback> cb;
ActiveTabTracker tracker(clock(), cb.Get());
tracker.AddTabStripModel(&model);
AddTab(&model);
AddTab(&model);
tracker.RemoveTabStripModel(&model);
EXPECT_CALL(cb, Run(_, _)).Times(0);
CloseTabAt(&model, 0);
model.DetachWebContentsAt(0);
}
} // namespace in_product_help
......@@ -3376,6 +3376,7 @@ test("unit_tests") {
"../browser/feature_engagement/incognito_window/incognito_window_tracker_unittest.cc",
"../browser/feature_engagement/new_tab/new_tab_tracker_unittest.cc",
"../browser/feature_engagement/session_duration_updater_unittest.cc",
"../browser/ui/in_product_help/active_tab_tracker_unittest.cc",
"../browser/ui/in_product_help/reopen_tab_iph_trigger_unittest.cc",
]
deps += [ "//components/feature_engagement/test:test_support" ]
......
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