// 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/ui/ash/network/tether_notification_presenter.h"
#include <algorithm>
#include <string>
#include "ash/constants/ash_features.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/network_icon_image_source.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/webui/settings/public/constants/routes.mojom.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "chrome/common/url_constants.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/network/network_connect.h"
#include "chromeos/ash/components/tether/pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_types.h"
#include "ui/message_center/public/cpp/notifier_id.h"
namespace ash::tether {
namespace {
const char kNotifierTether[] = "ash.tether";
// Mean value of NetworkState's signal_strength() range.
const int kMediumSignalStrength = 50;
// Dimensions of Tether notification icon in pixels.
constexpr gfx::Size kTetherSignalIconSize(18, 18);
// Handles clicking and closing of a notification via callbacks.
class TetherNotificationDelegate
: public message_center::HandleNotificationClickDelegate {
public:
TetherNotificationDelegate(ButtonClickCallback click,
base::RepeatingClosure close)
: HandleNotificationClickDelegate(click), close_callback_(close) {}
TetherNotificationDelegate(const TetherNotificationDelegate&) = delete;
TetherNotificationDelegate& operator=(const TetherNotificationDelegate&) =
delete;
// NotificationDelegate:
void Close(bool by_user) override {
if (!close_callback_.is_null()) {
close_callback_.Run();
}
}
private:
~TetherNotificationDelegate() override = default;
base::RepeatingClosure close_callback_;
};
class SettingsUiDelegateImpl
: public TetherNotificationPresenter::SettingsUiDelegate {
public:
SettingsUiDelegateImpl() = default;
~SettingsUiDelegateImpl() override = default;
void ShowSettingsSubPageForProfile(Profile* profile,
const std::string& sub_page) override {
chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(profile,
sub_page);
}
};
// Returns the icon to use for a network with the given signal strength, which
// should range from 0 to 100 (inclusive).
const gfx::ImageSkia GetImageForSignalStrength(int signal_strength) {
// Convert the [0, 100] range to [0, 4], since there are 5 distinct signal
// strength icons (0 bars to 4 bars).
int normalized_signal_strength = std::clamp(signal_strength / 25, 0, 4);
return gfx::CanvasImageSource::MakeImageSkia<
network_icon::SignalStrengthImageSource>(
network_icon::BARS, gfx::kGoogleBlue500, kTetherSignalIconSize,
normalized_signal_strength, 5);
}
} // namespace
// static
constexpr const char TetherNotificationPresenter::kActiveHostNotificationId[] =
"cros_tether_notification_ids.active_host";
// static
constexpr const char
TetherNotificationPresenter::kPotentialHotspotNotificationId[] =
"cros_tether_notification_ids.potential_hotspot";
// static
constexpr const char
TetherNotificationPresenter::kSetupRequiredNotificationId[] =
"cros_tether_notification_ids.setup_required";
// static
constexpr const char* const
TetherNotificationPresenter::kIdsWhichOpenTetherSettingsOnClick[] = {
TetherNotificationPresenter::kActiveHostNotificationId,
TetherNotificationPresenter::kPotentialHotspotNotificationId,
TetherNotificationPresenter::kSetupRequiredNotificationId};
TetherNotificationPresenter::TetherNotificationPresenter(
Profile* profile,
NetworkConnect* network_connect)
: profile_(profile),
network_connect_(network_connect),
settings_ui_delegate_(base::WrapUnique(new SettingsUiDelegateImpl())) {}
TetherNotificationPresenter::~TetherNotificationPresenter() = default;
// static
void TetherNotificationPresenter::RegisterProfilePrefs(
PrefRegistrySimple* pref_registry) {
pref_registry->RegisterBooleanPref(prefs::kNotificationsEnabled, true);
}
void TetherNotificationPresenter::NotifyPotentialHotspotNearby(
const std::string& device_id,
const std::string& device_name,
int signal_strength) {
PA_LOG(VERBOSE) << "Displaying \"potential hotspot nearby\" notification for "
<< "device with name \"" << device_name << "\". "
<< "Notification ID = " << kPotentialHotspotNotificationId;
hotspot_nearby_device_id_ = std::make_unique<std::string>(device_id);
message_center::RichNotificationData rich_notification_data;
rich_notification_data.buttons.push_back(
message_center::ButtonInfo(l10n_util::GetStringUTF16(
IDS_TETHER_NOTIFICATION_WIFI_AVAILABLE_ONE_DEVICE_CONNECT)));
ShowNotification(CreateNotification(
kPotentialHotspotNotificationId,
NotificationCatalogName::kTetherPotentialHotspot,
l10n_util::GetStringUTF16(
IDS_TETHER_NOTIFICATION_WIFI_AVAILABLE_ONE_DEVICE_TITLE),
l10n_util::GetStringFUTF16(
IDS_TETHER_NOTIFICATION_WIFI_AVAILABLE_ONE_DEVICE_MESSAGE,
base::ASCIIToUTF16(device_name)),
GetImageForSignalStrength(signal_strength), rich_notification_data));
}
void TetherNotificationPresenter::NotifyMultiplePotentialHotspotsNearby() {
PA_LOG(VERBOSE) << "Displaying \"potential hotspot nearby\" notification for "
<< "multiple devices. Notification ID = "
<< kPotentialHotspotNotificationId;
hotspot_nearby_device_id_.reset();
ShowNotification(CreateNotification(
kPotentialHotspotNotificationId,
NotificationCatalogName::kTetherPotentialHotspot,
l10n_util::GetStringUTF16(
IDS_TETHER_NOTIFICATION_WIFI_AVAILABLE_MULTIPLE_DEVICES_TITLE),
l10n_util::GetStringUTF16(
IDS_TETHER_NOTIFICATION_WIFI_AVAILABLE_MULTIPLE_DEVICES_MESSAGE),
GetImageForSignalStrength(kMediumSignalStrength),
{} /* rich_notification_data */));
}
NotificationPresenter::PotentialHotspotNotificationState
TetherNotificationPresenter::GetPotentialHotspotNotificationState() {
if (showing_notification_id_ != kPotentialHotspotNotificationId) {
return NotificationPresenter::PotentialHotspotNotificationState::
NO_HOTSPOT_NOTIFICATION_SHOWN;
}
return hotspot_nearby_device_id_
? NotificationPresenter::PotentialHotspotNotificationState::
SINGLE_HOTSPOT_NEARBY_SHOWN
: NotificationPresenter::PotentialHotspotNotificationState::
MULTIPLE_HOTSPOTS_NEARBY_SHOWN;
}
void TetherNotificationPresenter::RemovePotentialHotspotNotification() {
RemoveNotificationIfVisible(kPotentialHotspotNotificationId);
}
void TetherNotificationPresenter::NotifySetupRequired(
const std::string& device_name,
int signal_strength) {
PA_LOG(VERBOSE) << "Displaying \"setup required\" notification. Notification "
<< "ID = " << kSetupRequiredNotificationId;
// Persist this notification until acted upon or dismissed, so that the user
// is aware that they need to complete setup on their phone.
message_center::RichNotificationData rich_notification_data;
rich_notification_data.never_timeout = true;
ShowNotification(CreateNotification(
kSetupRequiredNotificationId,
NotificationCatalogName::kTetherSetupRequired,
l10n_util::GetStringFUTF16(IDS_TETHER_NOTIFICATION_SETUP_REQUIRED_TITLE,
base::ASCIIToUTF16(device_name)),
l10n_util::GetStringFUTF16(IDS_TETHER_NOTIFICATION_SETUP_REQUIRED_MESSAGE,
base::ASCIIToUTF16(device_name)),
GetImageForSignalStrength(signal_strength), rich_notification_data));
}
void TetherNotificationPresenter::RemoveSetupRequiredNotification() {
RemoveNotificationIfVisible(kSetupRequiredNotificationId);
}
void TetherNotificationPresenter::NotifyConnectionToHostFailed() {
const std::string id = kActiveHostNotificationId;
PA_LOG(VERBOSE) << "Displaying \"connection attempt failed\" notification. "
<< "Notification ID = " << id;
ShowNotification(CreateSystemNotificationPtr(
message_center::NotificationType::NOTIFICATION_TYPE_SIMPLE, id,
features::IsInstantHotspotRebrandEnabled()
? l10n_util::GetStringUTF16(
IDS_TETHER_NOTIFICATION_CONNECTION_FAILED_TITLE)
: l10n_util::GetStringUTF16(
IDS_TETHER_NOTIFICATION_CONNECTION_FAILED_TITLE_LEGACY),
l10n_util::GetStringUTF16(
IDS_TETHER_NOTIFICATION_CONNECTION_FAILED_MESSAGE),
std::u16string() /* display_source */, GURL() /* origin_url */,
message_center::NotifierId(
message_center::NotifierType::SYSTEM_COMPONENT, kNotifierTether,
NotificationCatalogName::kTetherConnectionError),
{} /* rich_notification_data */,
new message_center::HandleNotificationClickDelegate(base::BindRepeating(
&TetherNotificationPresenter::OnNotificationClicked,
weak_ptr_factory_.GetWeakPtr(), id)),
kNotificationCellularAlertIcon,
message_center::SystemNotificationWarningLevel::WARNING));
}
void TetherNotificationPresenter::RemoveConnectionToHostFailedNotification() {
RemoveNotificationIfVisible(kActiveHostNotificationId);
}
void TetherNotificationPresenter::OnNotificationClicked(
const std::string& notification_id,
std::optional<int> button_index) {
if (button_index) {
DCHECK_EQ(kPotentialHotspotNotificationId, notification_id);
DCHECK_EQ(0, *button_index);
DCHECK(hotspot_nearby_device_id_);
UMA_HISTOGRAM_ENUMERATION(
"InstantTethering.NotificationInteractionType",
TetherNotificationPresenter::NOTIFICATION_BUTTON_TAPPED_HOST_NEARBY,
TetherNotificationPresenter::NOTIFICATION_INTERACTION_TYPE_MAX);
PA_LOG(VERBOSE) << "\"Potential hotspot nearby\" notification button was "
<< "clicked.";
network_connect_->ConnectToNetworkId(*hotspot_nearby_device_id_);
RemoveNotificationIfVisible(kPotentialHotspotNotificationId);
return;
}
UMA_HISTOGRAM_ENUMERATION(
"InstantTethering.NotificationInteractionType",
GetMetricValueForClickOnNotificationBody(notification_id),
TetherNotificationPresenter::NOTIFICATION_INTERACTION_TYPE_MAX);
OpenSettingsAndRemoveNotification(
chromeos::settings::mojom::kMobileDataNetworksSubpagePath,
notification_id);
}
TetherNotificationPresenter::NotificationInteractionType
TetherNotificationPresenter::GetMetricValueForClickOnNotificationBody(
const std::string& clicked_notification_id) const {
if (clicked_notification_id == kPotentialHotspotNotificationId &&
hotspot_nearby_device_id_.get()) {
return TetherNotificationPresenter::
NOTIFICATION_BODY_TAPPED_SINGLE_HOST_NEARBY;
}
if (clicked_notification_id == kPotentialHotspotNotificationId &&
!hotspot_nearby_device_id_.get()) {
return TetherNotificationPresenter::
NOTIFICATION_BODY_TAPPED_MULTIPLE_HOSTS_NEARBY;
}
if (clicked_notification_id == kSetupRequiredNotificationId) {
return TetherNotificationPresenter::NOTIFICATION_BODY_TAPPED_SETUP_REQUIRED;
}
if (clicked_notification_id == kActiveHostNotificationId) {
return TetherNotificationPresenter::
NOTIFICATION_BODY_TAPPED_CONNECTION_FAILED;
}
NOTREACHED_IN_MIGRATION();
return TetherNotificationPresenter::NOTIFICATION_INTERACTION_TYPE_MAX;
}
void TetherNotificationPresenter::OnNotificationClosed(
const std::string& notification_id) {
if (showing_notification_id_ == notification_id) {
showing_notification_id_.clear();
}
}
std::unique_ptr<message_center::Notification>
TetherNotificationPresenter::CreateNotification(
const std::string& id,
const NotificationCatalogName& catalog_name,
const std::u16string& title,
const std::u16string& message,
const gfx::ImageSkia& small_image,
const message_center::RichNotificationData& rich_notification_data) {
auto notification = std::make_unique<message_center::Notification>(
message_center::NotificationType::NOTIFICATION_TYPE_SIMPLE, id, title,
message, ui::ImageModel(), std::u16string() /* display_source */,
GURL() /* origin_url */,
message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT,
kNotifierTether, catalog_name),
rich_notification_data,
new TetherNotificationDelegate(
base::BindRepeating(
&TetherNotificationPresenter::OnNotificationClicked,
weak_ptr_factory_.GetWeakPtr(), id),
base::BindRepeating(
&TetherNotificationPresenter::OnNotificationClosed,
weak_ptr_factory_.GetWeakPtr(), id)));
notification->SetSmallImage(gfx::Image(small_image));
if (base::FeatureList::IsEnabled(ash::features::kInstantHotspotRebrand)) {
notification->set_never_timeout(true);
}
return notification;
}
void TetherNotificationPresenter::SetSettingsUiDelegateForTesting(
std::unique_ptr<SettingsUiDelegate> settings_ui_delegate) {
settings_ui_delegate_ = std::move(settings_ui_delegate);
}
void TetherNotificationPresenter::ShowNotification(
std::unique_ptr<message_center::Notification> notification) {
if (!AreNotificationsEnabled()) {
PA_LOG(INFO) << "Not showing notification with ID [" << notification->id()
<< "] since user has notifications disabled.";
return;
}
showing_notification_id_ = notification->id();
NotificationDisplayService::GetForProfile(profile_)->Display(
NotificationHandler::Type::TRANSIENT, *notification,
/*metadata=*/nullptr);
}
void TetherNotificationPresenter::OpenSettingsAndRemoveNotification(
const std::string& settings_subpage,
const std::string& notification_id) {
PA_LOG(VERBOSE) << "Notification with ID " << notification_id
<< " was clicked. "
<< "Opening settings subpage: " << settings_subpage;
settings_ui_delegate_->ShowSettingsSubPageForProfile(profile_,
settings_subpage);
RemoveNotificationIfVisible(notification_id);
}
void TetherNotificationPresenter::RemoveNotificationIfVisible(
const std::string& notification_id) {
if (notification_id == kPotentialHotspotNotificationId) {
hotspot_nearby_device_id_.reset();
}
NotificationDisplayService::GetForProfile(profile_)->Close(
NotificationHandler::Type::TRANSIENT, notification_id);
}
bool TetherNotificationPresenter::AreNotificationsEnabled() {
return profile_->GetPrefs()->GetBoolean(prefs::kNotificationsEnabled);
}
} // namespace ash::tether