Commit 76e47ebc authored by Xing Liu's avatar Xing Liu Committed by Commit Bot

Notification scheduler: Implement logic to select notification to show.

This CL introduces a core logic class to pick scheduled notifications
to show.

Basically it checks the configuration and impression history, and
iterate all types of notification to return a list of notification.

Also implements a unit test frame work for this class, more test cases
will be added in following CLs.

Bug: 930968
Change-Id: Ia35519fe7a8509c53fa84ab380d42214c59d4e75
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1546672
Commit-Queue: Xing Liu <xingliu@chromium.org>
Reviewed-by: default avatarDavid Trainor <dtrainor@chromium.org>
Cr-Commit-Position: refs/heads/master@{#646648}
parent 284a0556
......@@ -49,6 +49,8 @@ source_set("lib") {
sources = [
"collection_store.h",
"display_decider.cc",
"display_decider.h",
"distribution_policy.cc",
"distribution_policy.h",
"icon_entry.h",
......@@ -89,6 +91,7 @@ source_set("lib") {
source_set("unit_tests") {
testonly = true
sources = [
"display_decider_unittest.cc",
"distribution_policy_unittest.cc",
"icon_store_unittest.cc",
"impression_history_tracker_unittest.cc",
......
// Copyright 2019 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/notifications/scheduler/display_decider.h"
#include <algorithm>
#include "chrome/browser/notifications/scheduler/distribution_policy.h"
#include "chrome/browser/notifications/scheduler/impression_types.h"
#include "chrome/browser/notifications/scheduler/notification_entry.h"
#include "chrome/browser/notifications/scheduler/scheduler_config.h"
#include "chrome/browser/notifications/scheduler/scheduler_utils.h"
using Notifications = notifications::DisplayDecider::Notifications;
using Results = notifications::DisplayDecider::Results;
using TypeStates = notifications::DisplayDecider::TypeStates;
namespace notifications {
namespace {
// Helper class contains the actual logic to decide which notifications to show.
// This is an one-shot class, callers should create a new object each time.
class DecisionHelper {
public:
DecisionHelper(const SchedulerConfig* config,
const std::vector<SchedulerClientType>& clients,
std::unique_ptr<DistributionPolicy> distribution_policy,
SchedulerTaskTime task_start_time,
Notifications notifications,
TypeStates type_states)
: notifications_(std::move(notifications)),
current_task_start_time_(task_start_time),
type_states_(std::move(type_states)),
config_(config),
clients_(clients),
policy_(std::move(distribution_policy)),
daily_max_to_show_all_types_(0),
last_shown_type_(SchedulerClientType::kUnknown),
shown_(0) {}
~DecisionHelper() = default;
// Figures out a list of notifications to show.
void DecideNotificationToShow(Results* results) {
ComputeDailyQuotaAllTypes();
CountNotificationsShownToday();
PickNotificationToShow(results);
}
private:
void ComputeDailyQuotaAllTypes() {
// Only background task launch needs to comply to |policy_|.
if (current_task_start_time_ == SchedulerTaskTime::kUnknown) {
daily_max_to_show_all_types_ = config_->max_daily_shown_all_type;
return;
}
int quota = std::max(config_->max_daily_shown_all_type - shown_, 0);
DCHECK(policy_);
daily_max_to_show_all_types_ =
std::min(config_->max_daily_shown_all_type,
policy_->MaxToShow(current_task_start_time_, quota));
}
void CountNotificationsShownToday() {
base::Time last_shown_time;
base::Time now(base::Time::Now());
base::Time beginning_of_today;
bool success = ToLocalHour(0, now, 0, &beginning_of_today);
DCHECK(success);
for (const auto& state : type_states_) {
const auto* type_state = state.second;
// TODO(xingliu): Ensure deprecated clients will not have data in storage.
DCHECK(std::find(clients_.begin(), clients_.end(), type_state->type) !=
clients_.end());
for (const auto& impression_it : type_state->impressions) {
const auto& impression = impression_it.second;
// Tracks last notification shown to the user.
if (impression.create_time > last_shown_time) {
last_shown_time = impression.create_time;
last_shown_type_ = type_state->type;
}
// Count notification shown today.
if (impression.create_time >= beginning_of_today) {
shown_per_type_[type_state->type]++;
++shown_;
}
}
}
}
// Picks a list of notifications to show.
void PickNotificationToShow(Results* to_show) {
DCHECK(to_show);
if (shown_ > config_->max_daily_shown_all_type || clients_.empty())
return;
// No previous shown notification, move the iterator to last element.
// We will iterate through all client types later.
auto it = std::find(clients_.begin(), clients_.end(), last_shown_type_);
if (it == clients_.end()) {
DCHECK_EQ(last_shown_type_, SchedulerClientType::kUnknown);
last_shown_type_ = clients_.back();
it = clients_.end() - 1;
}
DCHECK_NE(last_shown_type_, SchedulerClientType::kUnknown);
size_t steps = 0u;
// Circling around all clients to find new notification to show.
// TODO(xingliu): Apply scheduling parameters here.
do {
// Move the iterator to next client type.
DCHECK(it != clients_.end());
if (++it == clients_.end())
it = clients_.begin();
++steps;
SchedulerClientType type = *it;
// Check quota for all types and current background task type.
if (ReachDailyQuota())
break;
// Check quota for this type, and continue to iterate other types.
if (NoMoreNotificationToShow(type))
continue;
// Show the last notification in the vector. Notice the order depends on
// how the vector is sorted.
to_show->emplace(notifications_[type].back()->guid);
notifications_[type].pop_back();
shown_per_type_[type]++;
shown_++;
steps = 0u;
// Stop if we didn't find anything new to show, and have looped around
// all clients.
} while (steps <= clients_.size());
}
bool NoMoreNotificationToShow(SchedulerClientType type) {
auto it = type_states_.find(type);
int max_daily_show =
it == type_states_.end() ? 0 : it->second->current_max_daily_show;
return notifications_[type].empty() ||
shown_per_type_[type] >= config_->max_daily_shown_per_type ||
shown_per_type_[type] >= max_daily_show;
}
bool ReachDailyQuota() const {
return shown_ >= daily_max_to_show_all_types_;
}
// Scheduled notifications as candidates to display to the user.
Notifications notifications_;
const SchedulerTaskTime current_task_start_time_;
const TypeStates type_states_;
const SchedulerConfig* config_;
const std::vector<SchedulerClientType> clients_;
std::unique_ptr<DistributionPolicy> policy_;
int daily_max_to_show_all_types_;
SchedulerClientType last_shown_type_;
std::map<SchedulerClientType, int> shown_per_type_;
int shown_;
DISALLOW_COPY_AND_ASSIGN(DecisionHelper);
};
class DisplayDeciderImpl : public DisplayDecider {
public:
DisplayDeciderImpl() = default;
~DisplayDeciderImpl() override = default;
private:
// DisplayDecider implementation.
void FindNotificationsToShow(
const SchedulerConfig* config,
std::vector<SchedulerClientType> clients,
std::unique_ptr<DistributionPolicy> distribution_policy,
SchedulerTaskTime task_start_time,
Notifications notifications,
TypeStates type_states,
Results* results) override {
auto helper = std::make_unique<DecisionHelper>(
config, std::move(clients), std::move(distribution_policy),
task_start_time, std::move(notifications), std::move(type_states));
helper->DecideNotificationToShow(results);
}
DISALLOW_COPY_AND_ASSIGN(DisplayDeciderImpl);
};
} // namespace
// static
std::unique_ptr<DisplayDecider> DisplayDecider::Create() {
return std::make_unique<DisplayDeciderImpl>();
}
} // namespace notifications
// Copyright 2019 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_NOTIFICATIONS_SCHEDULER_DISPLAY_DECIDER_H_
#define CHROME_BROWSER_NOTIFICATIONS_SCHEDULER_DISPLAY_DECIDER_H_
#include <map>
#include <memory>
#include <set>
#include <utility>
#include <vector>
#include "base/callback.h"
#include "base/macros.h"
#include "chrome/browser/notifications/scheduler/internal_types.h"
#include "chrome/browser/notifications/scheduler/notification_scheduler_types.h"
namespace notifications {
class DistributionPolicy;
struct TypeState;
struct NotificationEntry;
struct SchedulerConfig;
// This class uses scheduled notifications data and notification impression data
// of each notification type to find a list of notification that should be
// displayed to the user.
// All operations should be done on the main thread.
class DisplayDecider {
public:
using Notifications =
std::map<SchedulerClientType, std::vector<const NotificationEntry*>>;
using TypeStates = std::map<SchedulerClientType, const TypeState*>;
using Results = std::set<std::string>;
// Creates the decider to determine notifications to show.
static std::unique_ptr<DisplayDecider> Create();
DisplayDecider() = default;
virtual ~DisplayDecider() = default;
// Finds notifications to show. Returns a list of notification guids.
virtual void FindNotificationsToShow(
const SchedulerConfig* config,
std::vector<SchedulerClientType> clients,
std::unique_ptr<DistributionPolicy> distribution_policy,
SchedulerTaskTime task_start_time,
Notifications notifications,
TypeStates type_states,
Results* results) = 0;
private:
DISALLOW_COPY_AND_ASSIGN(DisplayDecider);
};
} // namespace notifications
#endif // CHROME_BROWSER_NOTIFICATIONS_SCHEDULER_DISPLAY_DECIDER_H_
// Copyright 2019 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/notifications/scheduler/display_decider.h"
#include <map>
#include <memory>
#include <vector>
#include "base/strings/stringprintf.h"
#include "base/test/scoped_task_environment.h"
#include "chrome/browser/notifications/scheduler/distribution_policy.h"
#include "chrome/browser/notifications/scheduler/notification_entry.h"
#include "chrome/browser/notifications/scheduler/notification_scheduler_types.h"
#include "chrome/browser/notifications/scheduler/scheduler_config.h"
#include "chrome/browser/notifications/scheduler/test/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace notifications {
namespace {
// Default suppression info used in this test.
const SuppressionInfo kSuppressionInfo =
SuppressionInfo(base::Time::Now(), base::TimeDelta::FromDays(56));
// Initial state for test cases with a single registered client.
const std::vector<test::ImpressionTestData> kSingleClientImpressionTestData = {
{SchedulerClientType::kTest1,
2 /* current_max_daily_show */,
{},
base::nullopt /* suppression_info */}};
const std::vector<test::ImpressionTestData> kClientsImpressionTestData = {
{SchedulerClientType::kTest1,
2 /* current_max_daily_show */,
{},
base::nullopt /* suppression_info */},
{SchedulerClientType::kTest2,
5 /* current_max_daily_show */,
{},
base::nullopt /* suppression_info */},
{SchedulerClientType::kTest3,
0 /* current_max_daily_show */,
{},
kSuppressionInfo}};
struct TestData {
// Impression data as the input.
std::vector<test::ImpressionTestData> impression_test_data;
// Notification entries as the input.
std::vector<NotificationEntry> notification_entries;
// The type of current background task.
SchedulerTaskTime task_start_time = SchedulerTaskTime::kUnknown;
// Expected output data.
DisplayDecider::Results expected;
};
std::string DebugString(const DisplayDecider::Results& results) {
std::string debug_string("notifications_to_show: \n");
for (const auto& guid : results)
debug_string += base::StringPrintf("%s ", guid.c_str());
return debug_string;
}
// TODO(xingliu): Add more test cases.
class DisplayDeciderTest : public testing::Test {
public:
DisplayDeciderTest() = default;
~DisplayDeciderTest() override = default;
void SetUp() override {
// Setup configuration used by this test.
config_.morning_task_hour = 7;
config_.evening_task_hour = 18;
config_.max_daily_shown_all_type = 3;
}
protected:
// Initializes a test case with input data.
void RunTestCase(const TestData& test_data) {
test_data_ = test_data;
test::AddImpressionTestData(test_data_.impression_test_data, &type_states_);
DisplayDecider::Notifications notifications;
for (const auto& entry : test_data_.notification_entries) {
notifications[entry.type].emplace_back(&entry);
}
std::vector<SchedulerClientType> clients;
std::map<SchedulerClientType, const TypeState*> type_states;
for (const auto& type : type_states_) {
type_states.emplace(type.first, type.second.get());
clients.emplace_back(type.first);
}
// Copy test inputs into |decider_|.
decider_ = DisplayDecider::Create();
decider_->FindNotificationsToShow(
&config_, std::move(clients), DistributionPolicy::Create(),
test_data_.task_start_time, std::move(notifications),
std::move(type_states), &results_);
// Verify output.
EXPECT_EQ(results_, test_data_.expected)
<< "Actual result: \n"
<< DebugString(results_) << " \n Expected result: \n"
<< DebugString(test_data_.expected);
}
private:
base::test::ScopedTaskEnvironment scoped_task_environment_;
TestData test_data_;
SchedulerConfig config_;
std::map<SchedulerClientType, std::unique_ptr<TypeState>> type_states_;
// Test target class and output.
std::unique_ptr<DisplayDecider> decider_;
DisplayDecider::Results results_;
DISALLOW_COPY_AND_ASSIGN(DisplayDeciderTest);
};
TEST_F(DisplayDeciderTest, NoNotification) {
TestData data{kClientsImpressionTestData,
{},
SchedulerTaskTime::kEvening,
DisplayDecider::Results()};
RunTestCase(data);
}
// Simple test case to verify new notifcaiton can be selected to show.
TEST_F(DisplayDeciderTest, PickNewMorning) {
NotificationEntry entry(SchedulerClientType::kTest1, "guid123");
DisplayDecider::Results expected = {"guid123"};
TestData data{kSingleClientImpressionTestData,
{entry},
SchedulerTaskTime::kMorning,
std::move(expected)};
RunTestCase(data);
}
} // namespace
} // namespace notifications
......@@ -30,6 +30,12 @@ TEST(DistributionPolicyTest, EvenDistributionHigherMorning) {
5);
EXPECT_EQ(MaxToShow(policy.get(), SchedulerTaskTime::kEvening, 4 /* quota */),
4);
// Test 0 quota.
EXPECT_EQ(MaxToShow(policy.get(), SchedulerTaskTime::kMorning, 0 /* quota */),
0);
EXPECT_EQ(MaxToShow(policy.get(), SchedulerTaskTime::kMorning, 0 /* quota */),
0);
}
} // namespace
......
......@@ -12,6 +12,8 @@ NotificationEntry::NotificationEntry(SchedulerClientType type,
const std::string& guid)
: type(type), guid(guid) {}
NotificationEntry::NotificationEntry(const NotificationEntry& other) = default;
NotificationEntry::~NotificationEntry() = default;
} // namespace notifications
......@@ -17,6 +17,7 @@ namespace notifications {
// record.
struct NotificationEntry {
NotificationEntry(SchedulerClientType type, const std::string& guid);
NotificationEntry(const NotificationEntry& other);
~NotificationEntry();
// The type of the notification.
......@@ -25,8 +26,10 @@ struct NotificationEntry {
// The unique id of the notification database entry.
std::string guid;
// Contains information to construct the notification.
NotificationData notification_data;
// Scheduling details.
ScheduleParams schedule_params;
};
......
......@@ -12,6 +12,7 @@ enum class SchedulerClientType {
// Test only values.
kTest1 = -1,
kTest2 = -2,
kTest3 = -3,
// Default value of client type.
kUnknown = 0,
......
......@@ -6,22 +6,26 @@
namespace notifications {
bool ToLocalYesterdayHour(int hour, const base::Time& today, base::Time* out) {
bool ToLocalHour(int hour,
const base::Time& today,
int day_delta,
base::Time* out) {
DCHECK_GE(hour, 0);
DCHECK_LE(hour, 23);
DCHECK(out);
// Gets the local time at |hour| in yesterday.
base::Time yesterday = today - base::TimeDelta::FromDays(1);
base::Time::Exploded yesterday_exploded;
yesterday.LocalExplode(&yesterday_exploded);
yesterday_exploded.hour = hour;
yesterday_exploded.minute = 0;
yesterday_exploded.second = 0;
yesterday_exploded.millisecond = 0;
base::Time another_day = today + base::TimeDelta::FromDays(day_delta);
base::Time::Exploded another_day_exploded;
another_day.LocalExplode(&another_day_exploded);
another_day_exploded.hour = hour;
another_day_exploded.minute = 0;
another_day_exploded.second = 0;
another_day_exploded.millisecond = 0;
// Converts local exploded time to time stamp.
return base::Time::FromLocalExploded(yesterday_exploded, out);
return base::Time::FromLocalExploded(another_day_exploded, out);
}
} // namespace notifications
......@@ -9,12 +9,16 @@
namespace notifications {
// Retrieves the time stamp of a certain hour at yesterday.
// Retrieves the time stamp of a certain hour at a certain day from today.
// |hour| must be in the range of [0, 23].
// |today| is a timestamp to define today, usually caller can directly pass in
// the current system time.
// |day_delta| is the different between the output date and today.
// Returns false if the conversion is failed.
bool ToLocalYesterdayHour(int hour, const base::Time& today, base::Time* out);
bool ToLocalHour(int hour,
const base::Time& today,
int day_delta,
base::Time* out);
} // namespace notifications
......
......@@ -11,20 +11,31 @@ namespace notifications {
namespace {
// Verifies we can get the correct time stamp at certain hour in yesterday.
TEST(SchedulerUtilsTest, ToLocalYesterdayHour) {
base::Time today, yesterday, expected;
TEST(SchedulerUtilsTest, ToLocalHour) {
base::Time today, another_day, expected;
// Retrieve a timestamp of yesterday at 6am.
// Timestamp of another day in the past.
EXPECT_TRUE(base::Time::FromString("10/15/07 12:45:12 PM", &today));
EXPECT_TRUE(ToLocalYesterdayHour(6, today, &yesterday));
EXPECT_TRUE(ToLocalHour(6, today, -1, &another_day));
EXPECT_TRUE(base::Time::FromString("10/14/07 06:00:00 AM", &expected));
EXPECT_EQ(expected, yesterday);
EXPECT_EQ(expected, another_day);
// Test an edge case, that the time is 0 am of a Monday.
EXPECT_TRUE(base::Time::FromString("03/25/19 00:00:00 AM", &today));
EXPECT_TRUE(ToLocalYesterdayHour(0, today, &yesterday));
EXPECT_TRUE(ToLocalHour(0, today, -1, &another_day));
EXPECT_TRUE(base::Time::FromString("03/24/19 00:00:00 AM", &expected));
EXPECT_EQ(expected, yesterday);
EXPECT_EQ(expected, another_day);
// Timestamp of the same day.
EXPECT_TRUE(base::Time::FromString("03/25/19 00:00:00 AM", &today));
EXPECT_TRUE(ToLocalHour(0, today, 0, &another_day));
EXPECT_TRUE(base::Time::FromString("03/25/19 00:00:00 AM", &expected));
EXPECT_EQ(expected, another_day);
// Timestamp of another day in the future.
EXPECT_TRUE(base::Time::FromString("03/25/19 06:35:27 AM", &today));
EXPECT_TRUE(ToLocalHour(16, today, 7, &another_day));
EXPECT_TRUE(base::Time::FromString("04/01/19 16:00:00 PM", &expected));
EXPECT_EQ(expected, another_day);
}
} // namespace
......
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