blob: 1e2aa0458abe1c2f5d2cd775ae45cb94e71e1312 [file] [log] [blame]
// Copyright 2016 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/android/ntp/content_suggestions_notifier_service.h"
#include <algorithm>
#include "base/android/application_status_listener.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/android/ntp/content_suggestions_notifier.h"
#include "chrome/browser/notifications/notification_handler.h"
#include "chrome/browser/ntp_snippets/ntp_snippets_metrics.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "components/ntp_snippets/content_suggestions_service.h"
#include "components/ntp_snippets/features.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/strings/grit/components_strings.h"
#include "components/variations/variations_associated_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
using ntp_snippets::Category;
using ntp_snippets::CategoryStatus;
using ntp_snippets::ContentSuggestion;
using ntp_snippets::ContentSuggestionsService;
using ntp_snippets::KnownCategories;
using ntp_snippets::kNotificationsDailyLimit;
using ntp_snippets::kNotificationsDefaultDailyLimit;
using ntp_snippets::kNotificationsDefaultPriority;
using ntp_snippets::kNotificationsFeature;
using ntp_snippets::kNotificationsKeepWhenFrontmostParam;
using ntp_snippets::kNotificationsOpenToNTPParam;
using ntp_snippets::kNotificationsPriorityParam;
using ntp_snippets::kNotificationsTextParam;
using ntp_snippets::kNotificationsTextValueAndMore;
using ntp_snippets::kNotificationsTextValueSnippet;
base::android::ApplicationState*
g_content_suggestions_notification_application_state_for_testing = nullptr;
namespace {
const char kNotificationIDWithinCategory[] =
"ContentSuggestionsNotificationIDWithinCategory";
gfx::Image CropSquare(const gfx::Image& image) {
if (image.IsEmpty()) {
return image;
}
const gfx::ImageSkia* skimage = image.ToImageSkia();
gfx::Rect bounds{{0, 0}, skimage->size()};
int size = std::min(bounds.width(), bounds.height());
bounds.ClampToCenteredSize({size, size});
return gfx::Image(gfx::ImageSkiaOperations::CreateTiledImage(
*skimage, bounds.x(), bounds.y(), bounds.width(), bounds.height()));
}
bool ShouldNotifyInState(base::android::ApplicationState state) {
if (g_content_suggestions_notification_application_state_for_testing) {
state = *g_content_suggestions_notification_application_state_for_testing;
}
switch (state) {
case base::android::APPLICATION_STATE_UNKNOWN:
case base::android::APPLICATION_STATE_HAS_RUNNING_ACTIVITIES:
return false;
case base::android::APPLICATION_STATE_HAS_PAUSED_ACTIVITIES:
case base::android::APPLICATION_STATE_HAS_STOPPED_ACTIVITIES:
case base::android::APPLICATION_STATE_HAS_DESTROYED_ACTIVITIES:
return true;
}
NOTREACHED();
return false;
}
int DayAsYYYYMMDD() {
base::Time::Exploded now{};
base::Time::Now().LocalExplode(&now);
return (now.year * 10000) + (now.month * 100) + now.day_of_month;
}
bool HaveQuotaForToday(PrefService* prefs) {
int today = DayAsYYYYMMDD();
int limit = variations::GetVariationParamByFeatureAsInt(
kNotificationsFeature, kNotificationsDailyLimit,
kNotificationsDefaultDailyLimit);
int sent =
prefs->GetInteger(prefs::kContentSuggestionsNotificationsSentDay) == today
? prefs->GetInteger(prefs::kContentSuggestionsNotificationsSentCount)
: 0;
return sent < limit;
}
void ConsumeQuota(PrefService* prefs) {
int sent =
prefs->GetInteger(prefs::kContentSuggestionsNotificationsSentCount);
int today = DayAsYYYYMMDD();
if (prefs->GetInteger(prefs::kContentSuggestionsNotificationsSentDay) !=
today) {
prefs->SetInteger(prefs::kContentSuggestionsNotificationsSentDay, today);
sent = 0; // Reset on day change.
}
prefs->SetInteger(prefs::kContentSuggestionsNotificationsSentCount, sent + 1);
}
} // namespace
class ContentSuggestionsNotifierService::NotifyingObserver
: public ContentSuggestionsService::Observer {
public:
NotifyingObserver(ContentSuggestionsService* service,
PrefService* prefs,
ContentSuggestionsNotifier* notifier)
: service_(service),
prefs_(prefs),
notifier_(notifier),
app_status_listener_(base::android::ApplicationStatusListener::New(
base::BindRepeating(&NotifyingObserver::AppStatusChanged,
base::Unretained(this)))),
weak_ptr_factory_(this) {}
void OnNewSuggestions(Category category) override {
if (!ShouldNotifyInState(app_status_listener_->GetState())) {
DVLOG(1) << "Suppressed notification because Chrome is frontmost";
return;
} else if (!ContentSuggestionsNotifier::ShouldSendNotifications(prefs_)) {
DVLOG(1) << "Suppressed notification due to opt-out";
return;
} else if (!HaveQuotaForToday(prefs_)) {
DVLOG(1) << "Notification suppressed due to daily limit";
return;
}
const ContentSuggestion* suggestion = GetSuggestionToNotifyAbout(category);
if (!suggestion) {
return;
}
base::Time timeout_at = suggestion->notification_extra()
? suggestion->notification_extra()->deadline
: base::Time::Max();
const std::string text_param = variations::GetVariationParamValueByFeature(
kNotificationsFeature, kNotificationsTextParam);
base::string16 text;
if (text_param == kNotificationsTextValueSnippet) {
text = suggestion->snippet_text();
} else if (text_param == kNotificationsTextValueAndMore) {
int extra_count =
service_->GetSuggestionsForCategory(category).size() - 1;
text = l10n_util::GetStringFUTF16(
IDS_NTP_NOTIFICATIONS_READ_THIS_STORY_AND_MORE,
suggestion->publisher_name(), base::IntToString16(extra_count));
} else {
text = suggestion->publisher_name();
}
bool open_to_ntp = variations::GetVariationParamByFeatureAsBool(
kNotificationsFeature, kNotificationsOpenToNTPParam, false);
service_->FetchSuggestionImage(
suggestion->id(),
base::Bind(
&NotifyingObserver::ImageFetched, weak_ptr_factory_.GetWeakPtr(),
suggestion->id(),
open_to_ntp ? GURL(chrome::kChromeUINewTabURL) : suggestion->url(),
suggestion->title(), text, timeout_at));
}
void OnCategoryStatusChanged(Category category,
CategoryStatus new_status) override {
if (!category.IsKnownCategory(KnownCategories::ARTICLES)) {
return;
}
if (!ntp_snippets::IsCategoryStatusAvailable(new_status)) {
notifier_->HideAllNotifications(
ContentSuggestionsNotificationAction::HIDE_DISABLED);
}
}
void OnSuggestionInvalidated(
const ContentSuggestion::ID& suggestion_id) override {
notifier_->HideNotification(
suggestion_id, ContentSuggestionsNotificationAction::HIDE_EXPIRY);
}
void OnFullRefreshRequired() override {
notifier_->HideAllNotifications(
ContentSuggestionsNotificationAction::HIDE_EXPIRY);
}
void ContentSuggestionsServiceShutdown() override {
notifier_->HideAllNotifications(
ContentSuggestionsNotificationAction::HIDE_SHUTDOWN);
}
private:
const ContentSuggestion* GetSuggestionToNotifyAbout(Category category) {
const auto& suggestions = service_->GetSuggestionsForCategory(category);
for (const ContentSuggestion& suggestion : suggestions) {
if (suggestion.notification_extra()) {
return &suggestion;
}
}
return nullptr;
}
void AppStatusChanged(base::android::ApplicationState state) {
if (variations::GetVariationParamByFeatureAsBool(
kNotificationsFeature, kNotificationsKeepWhenFrontmostParam,
true)) {
return;
}
if (!ShouldNotifyInState(state)) {
notifier_->HideAllNotifications(
ContentSuggestionsNotificationAction::HIDE_FRONTMOST);
}
}
void ImageFetched(const ContentSuggestion::ID& id,
const GURL& url,
const base::string16& title,
const base::string16& text,
base::Time timeout_at,
const gfx::Image& image) {
if (!ShouldNotifyInState(app_status_listener_->GetState())) {
return; // Became foreground while we were fetching the image; forget it.
}
// check if suggestion is still valid.
DVLOG(1) << "Fetched " << image.Size().width() << "x"
<< image.Size().height() << " image for " << url.spec();
ConsumeQuota(prefs_);
int priority = variations::GetVariationParamByFeatureAsInt(
kNotificationsFeature, kNotificationsPriorityParam,
kNotificationsDefaultPriority);
if (notifier_->SendNotification(id, url, title, text, CropSquare(image),
timeout_at, priority)) {
RecordContentSuggestionsNotificationImpression(
id.category().IsKnownCategory(KnownCategories::ARTICLES)
? ContentSuggestionsNotificationImpression::ARTICLE
: ContentSuggestionsNotificationImpression::NONARTICLE);
}
}
ContentSuggestionsService* const service_;
PrefService* const prefs_;
ContentSuggestionsNotifier* const notifier_;
std::unique_ptr<base::android::ApplicationStatusListener>
app_status_listener_;
base::WeakPtrFactory<NotifyingObserver> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(NotifyingObserver);
};
ContentSuggestionsNotifierService::ContentSuggestionsNotifierService(
PrefService* prefs,
ContentSuggestionsService* suggestions,
std::unique_ptr<ContentSuggestionsNotifier> notifier)
: prefs_(prefs),
suggestions_service_(suggestions),
notifier_(std::move(notifier)) {
notifier_->FlushCachedMetrics();
if (notifier_->RegisterChannel(
prefs_->GetBoolean(prefs::kContentSuggestionsNotificationsEnabled))) {
// Once there is a notification channel, this setting is no longer relevant
// and the UI to control it is gone. Set it to true so that notifications
// are sent unconditionally. If the channel is disabled, then notifications
// will be blocked at the system level, and the user can re-enable them in
// the system notification settings.
prefs_->SetBoolean(prefs::kContentSuggestionsNotificationsEnabled, true);
}
if (IsEnabled()) {
Enable();
} else {
Disable();
}
}
ContentSuggestionsNotifierService::~ContentSuggestionsNotifierService() =
default;
void ContentSuggestionsNotifierService::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterBooleanPref(prefs::kContentSuggestionsNotificationsEnabled,
true);
registry->RegisterIntegerPref(
prefs::kContentSuggestionsConsecutiveIgnoredPrefName, 0);
registry->RegisterIntegerPref(prefs::kContentSuggestionsNotificationsSentDay,
0);
registry->RegisterIntegerPref(
prefs::kContentSuggestionsNotificationsSentCount, 0);
// TODO(sfiera): remove after M62; no longer (and never really) used.
registry->RegisterStringPref(kNotificationIDWithinCategory, std::string());
}
void ContentSuggestionsNotifierService::SetEnabled(bool enabled) {
prefs_->SetBoolean(prefs::kContentSuggestionsNotificationsEnabled, enabled);
if (enabled) {
Enable();
} else {
Disable();
RecordContentSuggestionsNotificationOptOut(
ContentSuggestionsNotificationOptOut::EXPLICIT);
}
}
bool ContentSuggestionsNotifierService::IsEnabled() const {
return prefs_->GetBoolean(prefs::kContentSuggestionsNotificationsEnabled);
}
void ContentSuggestionsNotifierService::Enable() {
if (!observer_) {
observer_.reset(
new NotifyingObserver(suggestions_service_, prefs_, notifier_.get()));
suggestions_service_->AddObserver(observer_.get());
}
}
void ContentSuggestionsNotifierService::Disable() {
if (observer_) {
suggestions_service_->RemoveObserver(observer_.get());
observer_.reset();
}
}