// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/notifications/notification_channels_provider_android.h"
#include <algorithm>
#include <utility>
#include "base/android/build_info.h"
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/time/default_clock.h"
#include "base/values.h"
#include "chrome/common/pref_names.h"
#include "components/content_settings/core/browser/content_settings_pref_provider.h"
#include "components/content_settings/core/browser/content_settings_rule.h"
#include "components/content_settings/core/browser/content_settings_utils.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/content_settings/core/common/content_settings_constraints.h"
#include "components/content_settings/core/common/content_settings_metadata.h"
#include "components/content_settings/core/common/content_settings_partition_key.h"
#include "components/content_settings/core/common/content_settings_utils.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/search_engines/template_url_service.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_constants.h"
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chrome/browser/notifications/jni_headers/NotificationSettingsBridge_jni.h"
using base::android::AttachCurrentThread;
using base::android::BuildInfo;
using base::android::ConvertJavaStringToUTF8;
using base::android::ConvertUTF8ToJavaString;
using base::android::JavaParamRef;
using base::android::ScopedJavaLocalRef;
namespace {
class NotificationChannelsBridgeImpl
: public NotificationChannelsProviderAndroid::NotificationChannelsBridge {
public:
NotificationChannelsBridgeImpl() = default;
~NotificationChannelsBridgeImpl() override = default;
NotificationChannel CreateChannel(const std::string& origin,
const base::Time& timestamp,
bool enabled) override {
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> jchannel =
Java_NotificationSettingsBridge_createChannel(
env, ConvertUTF8ToJavaString(env, origin),
timestamp.ToInternalValue(), enabled);
return NotificationChannel(
ConvertJavaStringToUTF8(Java_SiteChannel_getId(env, jchannel)),
ConvertJavaStringToUTF8(Java_SiteChannel_getOrigin(env, jchannel)),
base::Time::FromInternalValue(
Java_SiteChannel_getTimestamp(env, jchannel)),
static_cast<NotificationChannelStatus>(
Java_SiteChannel_getStatus(env, jchannel)));
}
void DeleteChannel(const std::string& origin) override {
JNIEnv* env = AttachCurrentThread();
Java_NotificationSettingsBridge_deleteChannel(
env, ConvertUTF8ToJavaString(env, origin));
}
void GetChannels(NotificationChannelsProviderAndroid::GetChannelsCallback
callback) override {
JNIEnv* env = AttachCurrentThread();
NotificationChannelsProviderAndroid::GetChannelsCallback cb =
base::BindOnce(&NotificationChannelsBridgeImpl::OnGetChannelsDone,
weak_factory_.GetWeakPtr(), std::move(callback));
intptr_t callback_id = reinterpret_cast<intptr_t>(
new NotificationChannelsProviderAndroid::GetChannelsCallback(
std::move(cb)));
Java_NotificationSettingsBridge_getSiteChannels(env, callback_id);
}
private:
void OnGetChannelsDone(
NotificationChannelsProviderAndroid::GetChannelsCallback callback,
const std::vector<NotificationChannel>& channels) {
std::move(callback).Run(channels);
}
base::WeakPtrFactory<NotificationChannelsBridgeImpl> weak_factory_{this};
};
ContentSetting ChannelStatusToContentSetting(NotificationChannelStatus status) {
switch (status) {
case NotificationChannelStatus::ENABLED:
return CONTENT_SETTING_ALLOW;
case NotificationChannelStatus::BLOCKED:
return CONTENT_SETTING_BLOCK;
case NotificationChannelStatus::UNAVAILABLE:
NOTREACHED_IN_MIGRATION();
}
return CONTENT_SETTING_DEFAULT;
}
class ChannelsRuleIterator : public content_settings::RuleIterator {
public:
explicit ChannelsRuleIterator(std::vector<NotificationChannel> channels)
: channels_(std::move(channels)) {}
ChannelsRuleIterator(const ChannelsRuleIterator&) = delete;
ChannelsRuleIterator& operator=(const ChannelsRuleIterator&) = delete;
~ChannelsRuleIterator() override = default;
bool HasNext() const override { return index_ < channels_.size(); }
std::unique_ptr<content_settings::Rule> Next() override {
DCHECK(HasNext());
auto& channel = channels_[index_];
DCHECK_NE(channels_[index_].status, NotificationChannelStatus::UNAVAILABLE);
content_settings::RuleMetaData metadata;
metadata.set_last_modified(channel.timestamp);
std::unique_ptr<content_settings::Rule> rule =
std::make_unique<content_settings::Rule>(
ContentSettingsPattern::FromURLNoWildcard(GURL(channel.origin)),
ContentSettingsPattern::Wildcard(),
base::Value(ChannelStatusToContentSetting(channel.status)),
metadata);
index_++;
return rule;
}
private:
std::vector<NotificationChannel> channels_;
size_t index_ = 0;
};
// This copies the logic of
// SearchPermissionsService::IsPermissionControlledByDSE, which cannot be
// called from this class as it would introduce a circular dependency between
// the HostContentSettingsMap and the SearchPermissionsService factories.
bool OriginMatchesDefaultSearchEngine(const GURL& default_search_engine_url,
const std::string& origin) {
if (default_search_engine_url.is_empty()) {
return false;
}
return url::IsSameOriginWith(GURL(origin), default_search_engine_url);
}
} // anonymous namespace
static void JNI_NotificationSettingsBridge_OnGetSiteChannelsDone(
JNIEnv* env,
jlong callback_id,
const JavaParamRef<jobjectArray>& j_channels) {
std::vector<NotificationChannel> channels;
for (auto jchannel : j_channels.ReadElements<jobject>()) {
channels.emplace_back(
ConvertJavaStringToUTF8(Java_SiteChannel_getId(env, jchannel)),
ConvertJavaStringToUTF8(Java_SiteChannel_getOrigin(env, jchannel)),
base::Time::FromInternalValue(
Java_SiteChannel_getTimestamp(env, jchannel)),
static_cast<NotificationChannelStatus>(
Java_SiteChannel_getStatus(env, jchannel)));
}
// Convert java long long int to c++ pointer, take ownership.
std::unique_ptr<NotificationChannelsProviderAndroid::GetChannelsCallback> cb(
reinterpret_cast<
NotificationChannelsProviderAndroid::GetChannelsCallback*>(
callback_id));
std::move(*cb).Run(std::move(channels));
}
// static
void NotificationChannelsProviderAndroid::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterBooleanPref(prefs::kClearedBlockedSiteNotificationChannels,
false /* default_value */);
registry->RegisterBooleanPref(prefs::kMigratedToSiteNotificationChannels,
false);
}
NotificationChannel::NotificationChannel(const std::string& id,
const std::string& origin,
const base::Time& timestamp,
NotificationChannelStatus status)
: id(id), origin(origin), timestamp(timestamp), status(status) {}
NotificationChannel::NotificationChannel(const NotificationChannel& other) =
default;
NotificationChannelsProviderAndroid::NotificationChannelsProviderAndroid(
PrefService* pref_service)
: NotificationChannelsProviderAndroid(
pref_service,
std::make_unique<NotificationChannelsBridgeImpl>()) {}
NotificationChannelsProviderAndroid::NotificationChannelsProviderAndroid(
PrefService* pref_service,
std::unique_ptr<NotificationChannelsBridge> bridge)
: bridge_(std::move(bridge)),
clock_(base::DefaultClock::GetInstance()),
pref_service_(pref_service) {}
NotificationChannelsProviderAndroid::~NotificationChannelsProviderAndroid() =
default;
void NotificationChannelsProviderAndroid::Initialize(
content_settings::ProviderInterface* pref_provider,
TemplateURLService* template_url_service) {
MigrateToChannelsIfNecessary(pref_provider);
// Clear blocked channels *after* migrating in case the pref provider
// contained any erroneously-created channels that need deleting.
ClearBlockedChannelsIfNecessary(template_url_service);
InitCachedChannels(base::DoNothing());
}
void NotificationChannelsProviderAndroid::MigrateToChannelsIfNecessary(
content_settings::ProviderInterface* pref_provider) {
if (pref_service_->GetBoolean(prefs::kMigratedToSiteNotificationChannels)) {
return;
}
InitCachedChannels(base::BindOnce(
&NotificationChannelsProviderAndroid::MigrateToChannelsIfNecessaryImpl,
weak_factory_.GetWeakPtr(), base::Unretained(pref_provider)));
}
void NotificationChannelsProviderAndroid::MigrateToChannelsIfNecessaryImpl(
content_settings::ProviderInterface* pref_provider) {
std::vector<std::pair<ContentSettingsPattern, ContentSettingsPattern>>
patterns;
// Collect the existing rules and create channels for them.
{
std::unique_ptr<content_settings::RuleIterator> it(
pref_provider->GetRuleIterator(
ContentSettingsType::NOTIFICATIONS, false /* incognito */,
content_settings::PartitionKey::WipGetDefault()));
while (it && it->HasNext()) {
std::unique_ptr<content_settings::Rule> rule = it->Next();
CreateChannelForRule(*rule);
patterns.emplace_back(std::move(rule->primary_pattern),
std::move(rule->secondary_pattern));
}
}
// Remove the existing |rules| from the preference provider.
for (const auto& pattern : patterns) {
pref_provider->SetWebsiteSetting(
pattern.first, pattern.second, ContentSettingsType::NOTIFICATIONS,
base::Value(), {}, content_settings::PartitionKey::WipGetDefault());
}
pref_service_->SetBoolean(prefs::kMigratedToSiteNotificationChannels, true);
}
void NotificationChannelsProviderAndroid::ClearBlockedChannelsIfNecessary(
TemplateURLService* template_url_service) {
if (pref_service_->GetBoolean(
prefs::kClearedBlockedSiteNotificationChannels)) {
return;
}
ScheduleGetChannels(
/*skip_get_if_cached_channels_are_available=*/false,
base::BindOnce(&NotificationChannelsProviderAndroid::
ClearBlockedChannelsIfNecessaryImpl,
weak_factory_.GetWeakPtr(),
base::Unretained(template_url_service)));
}
void NotificationChannelsProviderAndroid::ClearBlockedChannelsIfNecessaryImpl(
TemplateURLService* template_url_service,
const std::vector<NotificationChannel>& channels) {
GURL default_search_engine_url;
if (template_url_service &&
template_url_service->GetDefaultSearchProvider()) {
default_search_engine_url =
template_url_service->GetDefaultSearchProvider()->GenerateSearchURL(
template_url_service->search_terms_data());
}
for (const NotificationChannel& channel : channels) {
if (channel.status != NotificationChannelStatus::BLOCKED)
continue;
if (OriginMatchesDefaultSearchEngine(default_search_engine_url,
channel.origin)) {
// Do not clear the DSE permission, as it should always be ALLOW or BLOCK.
continue;
}
bridge_->DeleteChannel(channel.id);
}
// Reset the cache.
cached_channels_.reset();
pref_service_->SetBoolean(prefs::kClearedBlockedSiteNotificationChannels,
true);
}
std::unique_ptr<content_settings::RuleIterator>
NotificationChannelsProviderAndroid::GetRuleIterator(
ContentSettingsType content_type,
bool incognito,
const content_settings::PartitionKey& partition_key) const {
if (content_type != ContentSettingsType::NOTIFICATIONS || incognito) {
return nullptr;
}
// This const_cast is not ideal but tolerated because it allows us to
// notify observers as soon as we detect changes to channels.
auto* provider = const_cast<NotificationChannelsProviderAndroid*>(this);
provider->RecordCachedChannelStatus();
if (!cached_channels_) {
return nullptr;
}
std::vector<NotificationChannel> channels;
for (const auto& cached_channel : *cached_channels_) {
channels.push_back(cached_channel.second);
}
// The returned RuleIterator is from cached channels, so it might not
// contain up-to-date information if user has modified notification settings,
// As a result, schedule an channel update to inform all observers if
// something has changed.
provider->ScheduleGetChannels(
/*skip_get_if_cached_channels_are_available=*/false,
base::BindOnce(
&NotificationChannelsProviderAndroid::UpdateCachedChannelsImpl,
provider->weak_factory_.GetWeakPtr(),
/*only_initialize_null_cached_channels=*/false, base::DoNothing()));
return channels.empty()
? nullptr
: std::make_unique<ChannelsRuleIterator>(std::move(channels));
}
bool NotificationChannelsProviderAndroid::SetWebsiteSetting(
const ContentSettingsPattern& primary_pattern,
const ContentSettingsPattern& secondary_pattern,
ContentSettingsType content_type,
base::Value&& value,
const content_settings::ContentSettingConstraints& constraints,
const content_settings::PartitionKey& partition_key) {
if (content_type != ContentSettingsType::NOTIFICATIONS) {
return false;
}
// This provider only handles settings for specific origins.
if (primary_pattern == ContentSettingsPattern::Wildcard() &&
secondary_pattern == ContentSettingsPattern::Wildcard()) {
return false;
}
// These constraints are not supported for notifications on Android.
DCHECK_EQ(constraints.expiration(), base::Time());
DCHECK_EQ(constraints.session_model(),
content_settings::mojom::SessionModel::DURABLE);
DCHECK_EQ(constraints.track_last_visit_for_autoexpiration(), false);
ContentSetting setting = content_settings::ValueToContentSetting(value);
InitCachedChannels(base::BindOnce(
&NotificationChannelsProviderAndroid::UpdateChannelForWebsiteImpl,
weak_factory_.GetWeakPtr(), primary_pattern, secondary_pattern,
content_type, setting, constraints));
if (setting == CONTENT_SETTING_DEFAULT) {
return false;
}
value = base::Value();
return true;
}
void NotificationChannelsProviderAndroid::UpdateChannelForWebsiteImpl(
const ContentSettingsPattern& primary_pattern,
const ContentSettingsPattern& secondary_pattern,
ContentSettingsType content_type,
ContentSetting content_setting,
const content_settings::ContentSettingConstraints& constraints) {
url::Origin origin = url::Origin::Create(GURL(primary_pattern.ToString()));
DCHECK(!origin.opaque());
const std::string origin_string = origin.Serialize();
switch (content_setting) {
case CONTENT_SETTING_ALLOW:
CreateChannelIfRequired(origin_string,
NotificationChannelStatus::ENABLED);
break;
case CONTENT_SETTING_BLOCK:
CreateChannelIfRequired(origin_string,
NotificationChannelStatus::BLOCKED);
break;
case CONTENT_SETTING_DEFAULT: {
auto channel_to_delete = cached_channels_->find(origin_string);
if (channel_to_delete != cached_channels_->end()) {
bridge_->DeleteChannel(channel_to_delete->second.id);
cached_channels_->erase(channel_to_delete);
NotifyObservers(primary_pattern, secondary_pattern, content_type,
/*partition_key=*/nullptr);
}
break;
}
default:
// We rely on notification settings being one of ALLOW/BLOCK/DEFAULT.
NOTREACHED_IN_MIGRATION();
break;
}
}
void NotificationChannelsProviderAndroid::ClearAllContentSettingsRules(
ContentSettingsType content_type,
const content_settings::PartitionKey& partition_key) {
if (content_type != ContentSettingsType::NOTIFICATIONS) {
return;
}
ScheduleGetChannels(
/*skip_get_if_cached_channels_are_available=*/false,
base::BindOnce(&NotificationChannelsProviderAndroid::ClearAllChannelsImpl,
weak_factory_.GetWeakPtr(), content_type));
}
void NotificationChannelsProviderAndroid::ClearAllChannelsImpl(
ContentSettingsType content_type,
const std::vector<NotificationChannel>& channels) {
for (auto channel : channels) {
bridge_->DeleteChannel(channel.id);
}
cached_channels_->clear();
if (channels.size() > 0) {
NotifyObservers(ContentSettingsPattern::Wildcard(),
ContentSettingsPattern::Wildcard(), content_type,
/*partition_key=*/nullptr);
}
}
void NotificationChannelsProviderAndroid::ShutdownOnUIThread() {
RemoveAllObservers();
}
bool NotificationChannelsProviderAndroid::UpdateLastUsedTime(
const GURL& primary_url,
const GURL& secondary_url,
ContentSettingsType content_type,
const base::Time time,
const content_settings::PartitionKey& partition_key) {
// Last used tracking is not implemented for this type.
return false;
}
bool NotificationChannelsProviderAndroid::ResetLastVisitTime(
const ContentSettingsPattern& primary_pattern,
const ContentSettingsPattern& secondary_pattern,
ContentSettingsType content_type,
const content_settings::PartitionKey& partition_key) {
// Last visited tracking is not implemented for this type.
return false;
}
bool NotificationChannelsProviderAndroid::UpdateLastVisitTime(
const ContentSettingsPattern& primary_pattern,
const ContentSettingsPattern& secondary_pattern,
ContentSettingsType content_type,
const content_settings::PartitionKey& partition_key) {
// Last visited tracking is not implemented for this type.
return false;
}
std::optional<base::TimeDelta>
NotificationChannelsProviderAndroid::RenewContentSetting(
const GURL& primary_url,
const GURL& secondary_url,
ContentSettingsType content_type,
std::optional<ContentSetting> setting_to_match,
const content_settings::PartitionKey& partition_key) {
// Setting renewal is not implemented for this type.
return std::nullopt;
}
void NotificationChannelsProviderAndroid::SetClockForTesting(
base::Clock* clock) {
clock_ = clock;
}
// InitCachedChannels() must be called prior to calling this method.
void NotificationChannelsProviderAndroid::CreateChannelIfRequired(
const std::string& origin_string,
NotificationChannelStatus new_channel_status) {
auto channel_entry = cached_channels_->find(origin_string);
if (channel_entry == cached_channels_->end()) {
base::Time timestamp = clock_->Now();
NotificationChannel channel = bridge_->CreateChannel(
origin_string, timestamp,
new_channel_status == NotificationChannelStatus::ENABLED);
cached_channels_->emplace(origin_string, std::move(channel));
NotifyObservers(
ContentSettingsPattern::Wildcard(), ContentSettingsPattern::Wildcard(),
ContentSettingsType::NOTIFICATIONS, /*partition_key=*/nullptr);
}
}
// InitCachedChannels() must be called prior to calling this method.
void NotificationChannelsProviderAndroid::CreateChannelForRule(
const content_settings::Rule& rule) {
url::Origin origin =
url::Origin::Create(GURL(rule.primary_pattern.ToString()));
DCHECK(!origin.opaque());
const std::string origin_string = origin.Serialize();
ContentSetting content_setting =
content_settings::ValueToContentSetting(rule.value);
switch (content_setting) {
case CONTENT_SETTING_ALLOW:
CreateChannelIfRequired(origin_string,
NotificationChannelStatus::ENABLED);
break;
case CONTENT_SETTING_BLOCK:
CreateChannelIfRequired(origin_string,
NotificationChannelStatus::BLOCKED);
break;
default:
// We assume notification preferences are either ALLOW/BLOCK.
NOTREACHED_IN_MIGRATION();
break;
}
}
// This method must be called prior to accessing |cached_channels_|.
void NotificationChannelsProviderAndroid::InitCachedChannels(
base::OnceClosure on_channels_initialized_cb) {
ScheduleGetChannels(
/*skip_get_if_cached_channels_are_available=*/true,
base::BindOnce(
&NotificationChannelsProviderAndroid::UpdateCachedChannelsImpl,
weak_factory_.GetWeakPtr(),
/*only_initialize_null_cached_channels=*/true,
std::move(on_channels_initialized_cb)));
}
void NotificationChannelsProviderAndroid::UpdateCachedChannelsImpl(
bool only_initialize_null_cached_channels,
base::OnceClosure on_channel_updated_cb,
const std::vector<NotificationChannel>& channels) {
std::map<std::string, NotificationChannel> updated_channels_map;
for (const auto& channel : channels) {
updated_channels_map.emplace(channel.origin, channel);
}
if (only_initialize_null_cached_channels) {
if (!cached_channels_) {
cached_channels_ = std::move(updated_channels_map);
}
} else {
if (updated_channels_map != cached_channels_.value()) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&NotificationChannelsProviderAndroid::NotifyObservers,
weak_factory_.GetWeakPtr(),
ContentSettingsPattern::Wildcard(),
ContentSettingsPattern::Wildcard(),
ContentSettingsType::NOTIFICATIONS,
/*partition_key=*/nullptr));
cached_channels_ = std::move(updated_channels_map);
}
}
std::move(on_channel_updated_cb).Run();
}
void NotificationChannelsProviderAndroid::ScheduleGetChannels(
bool skip_get_if_cached_channels_are_available,
GetChannelsCallback get_channels_cb) {
pending_operations_.push(base::BindOnce(
&NotificationChannelsProviderAndroid::GetChannelsImpl,
weak_factory_.GetWeakPtr(), skip_get_if_cached_channels_are_available,
std::move(get_channels_cb)));
ProcessPendingOperations();
}
void NotificationChannelsProviderAndroid::GetChannelsImpl(
bool skip_get_if_cached_channels_are_available,
GetChannelsCallback get_channels_cb,
base::OnceClosure on_task_completed_cb) {
if (skip_get_if_cached_channels_are_available && cached_channels_) {
OnGetChannelsDone(std::move(get_channels_cb),
std::move(on_task_completed_cb),
std::vector<NotificationChannel>());
return;
}
bridge_->GetChannels(
base::BindOnce(&NotificationChannelsProviderAndroid::OnGetChannelsDone,
weak_factory_.GetWeakPtr(), std::move(get_channels_cb),
std::move(on_task_completed_cb)));
}
void NotificationChannelsProviderAndroid::OnGetChannelsDone(
GetChannelsCallback get_channels_cb,
base::OnceClosure on_task_completed_cb,
const std::vector<NotificationChannel>& channels) {
std::move(get_channels_cb).Run(channels);
std::move(on_task_completed_cb).Run();
}
void NotificationChannelsProviderAndroid::ProcessPendingOperations() {
if (is_processing_pending_operations_ || pending_operations_.empty()) {
return;
}
is_processing_pending_operations_ = true;
PendingCallback callback = std::move(pending_operations_.front());
pending_operations_.pop();
std::move(callback).Run(base::BindOnce(
&NotificationChannelsProviderAndroid::OnCurrentOperationFinished,
weak_factory_.GetWeakPtr()));
}
void NotificationChannelsProviderAndroid::OnCurrentOperationFinished() {
DCHECK(is_processing_pending_operations_);
is_processing_pending_operations_ = false;
ProcessPendingOperations();
}
void NotificationChannelsProviderAndroid::RecordCachedChannelStatus() {
if (!has_get_rule_iterator_called_) {
base::UmaHistogramBoolean(
"Notifications.Android.CachedChannelsStatusOnFirstGetRuleIterator",
!!cached_channels_);
has_get_rule_iterator_called_ = true;
}
}