| // 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/notifications/notification_display_service_factory.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(); |
| } |
| |
| 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(); |
| NotificationDisplayServiceFactory::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(); |
| } |
| |
| NotificationDisplayServiceFactory::GetForProfile(profile_)->Close( |
| NotificationHandler::Type::TRANSIENT, notification_id); |
| } |
| |
| bool TetherNotificationPresenter::AreNotificationsEnabled() { |
| return profile_->GetPrefs()->GetBoolean(prefs::kNotificationsEnabled); |
| } |
| |
| } // namespace ash::tether |