blob: e04276c1e7a872195122ca1e86f18205a79816db [file] [log] [blame]
// Copyright 2020 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 "ash/system/phonehub/phone_hub_notification_controller.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/public/cpp/system_tray_client.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/phonehub/phone_hub_metrics.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "base/bind.h"
#include "base/containers/contains.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/timer/timer.h"
#include "chromeos/components/phonehub/notification.h"
#include "chromeos/components/phonehub/phone_hub_manager.h"
#include "chromeos/components/phonehub/phone_model.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/text_elider.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_delegate.h"
#include "ui/message_center/views/message_view_factory.h"
#include "ui/message_center/views/notification_header_view.h"
#include "ui/message_center/views/notification_view_md.h"
#include "ui/views/controls/textfield/textfield.h"
namespace ash {
using phone_hub_metrics::NotificationInteraction;
namespace {
const char kNotifierId[] = "chrome://phonehub";
const char kNotifierIdSeparator[] = "-";
const char kPhoneHubInstantTetherNotificationId[] =
"chrome://phonehub-instant-tether";
const char kNotificationCustomViewType[] = "phonehub";
const int kReplyButtonIndex = 0;
const int kNotificationHeaderTextWidth = 180;
const int kNotificationAppNameMaxWidth = 140;
// The amount of time the reply button is disabled after sending an inline
// reply. This is used to make sure that all the replies are received by the
// phone in a correct order (a reply sent right after another could cause it to
// be received before the former one).
constexpr base::TimeDelta kInlineReplyDisableTime =
base::TimeDelta::FromSeconds(1);
class PhoneHubNotificationView : public message_center::NotificationViewMD {
public:
explicit PhoneHubNotificationView(
const message_center::Notification& notification,
const base::string16& phone_name)
: message_center::NotificationViewMD(notification) {
// Add customized header.
message_center::NotificationHeaderView* header_row =
static_cast<message_center::NotificationHeaderView*>(
GetViewByID(message_center::NotificationViewMD::kHeaderRow));
views::View* app_name_view =
GetViewByID(message_center::NotificationViewMD::kAppNameView);
views::Label* summary_text_view = static_cast<views::Label*>(
GetViewByID(message_center::NotificationViewMD::kSummaryTextView));
// The app name should be displayed in full, leaving the rest of the space
// for device name. App name will only be truncated when it reached it
// maximum width.
int app_name_width = std::min(app_name_view->GetPreferredSize().width(),
kNotificationAppNameMaxWidth);
int device_name_width = kNotificationHeaderTextWidth - app_name_width;
header_row->SetSummaryText(
gfx::ElideText(phone_name, summary_text_view->font_list(),
device_name_width, gfx::ELIDE_TAIL));
action_buttons_row_ =
GetViewByID(message_center::NotificationViewMD::kActionButtonsRow);
if (!action_buttons_row_->children().empty())
reply_button_ = static_cast<message_center::NotificationMdTextButton*>(
action_buttons_row_->children()[kReplyButtonIndex]);
inline_reply_ = static_cast<message_center::NotificationInputContainerMD*>(
GetViewByID(message_center::NotificationViewMD::kInlineReply));
}
~PhoneHubNotificationView() override = default;
PhoneHubNotificationView(const PhoneHubNotificationView&) = delete;
PhoneHubNotificationView& operator=(const PhoneHubNotificationView&) = delete;
// message_center::NotificationViewMD:
void OnNotificationInputSubmit(size_t index,
const base::string16& text) override {
message_center::NotificationViewMD::OnNotificationInputSubmit(index, text);
DCHECK(reply_button_);
// After sending a reply, take the UI back to action buttons and clear out
// text input.
inline_reply_->SetVisible(false);
action_buttons_row_->SetVisible(true);
inline_reply_->textfield()->SetText(base::string16());
// Briefly disable reply button.
reply_button_->SetEnabled(false);
enable_reply_timer_ = std::make_unique<base::OneShotTimer>();
enable_reply_timer_->Start(
FROM_HERE, kInlineReplyDisableTime,
base::BindOnce(&PhoneHubNotificationView::EnableReplyButton,
base::Unretained(this)));
}
void EnableReplyButton() {
reply_button_->SetEnabled(true);
enable_reply_timer_.reset();
}
private:
// Owned by view hierarchy.
views::View* action_buttons_row_ = nullptr;
message_center::NotificationMdTextButton* reply_button_ = nullptr;
message_center::NotificationInputContainerMD* inline_reply_ = nullptr;
// Timer that fires to enable reply button after a brief period of time.
std::unique_ptr<base::OneShotTimer> enable_reply_timer_;
};
} // namespace
// Delegate for the displayed ChromeOS notification.
class PhoneHubNotificationController::NotificationDelegate
: public message_center::NotificationObserver {
public:
NotificationDelegate(PhoneHubNotificationController* controller,
int64_t phone_hub_id,
const std::string& cros_id)
: controller_(controller),
phone_hub_id_(phone_hub_id),
cros_id_(cros_id) {}
virtual ~NotificationDelegate() { controller_ = nullptr; }
NotificationDelegate(const NotificationDelegate&) = delete;
NotificationDelegate& operator=(const NotificationDelegate&) = delete;
// Returns a scoped_refptr that can be passed in the
// message_center::Notification constructor.
scoped_refptr<message_center::NotificationDelegate> AsScopedRefPtr() {
return base::MakeRefCounted<message_center::ThunkNotificationDelegate>(
weak_ptr_factory_.GetWeakPtr());
}
// Called by the controller to remove the notification from the message
// center.
void Remove() {
removed_by_phone_hub_ = true;
message_center::MessageCenter::Get()->RemoveNotification(cros_id_,
/*by_user=*/false);
}
// message_center::NotificationObserver:
void Close(bool by_user) override {
if (controller_ && !removed_by_phone_hub_)
controller_->DismissNotification(phone_hub_id_);
}
void Click(const base::Optional<int>& button_index,
const base::Optional<base::string16>& reply) override {
if (!controller_ || !button_index.has_value())
return;
if (button_index.value() == kReplyButtonIndex && reply.has_value()) {
controller_->SendInlineReply(phone_hub_id_, reply.value());
return;
}
}
void SettingsClick() override {
if (controller_)
controller_->OpenSettings();
}
private:
// The parent controller, which owns this object.
PhoneHubNotificationController* controller_ = nullptr;
// The notification ID tracked by PhoneHub.
const int64_t phone_hub_id_;
// The notification ID tracked by the CrOS message center.
const std::string cros_id_;
// Flag set if the notification was removed by PhoneHub so we avoid a cycle.
bool removed_by_phone_hub_ = false;
base::WeakPtrFactory<NotificationDelegate> weak_ptr_factory_{this};
};
PhoneHubNotificationController::PhoneHubNotificationController() {
if (message_center::MessageViewFactory::HasCustomNotificationViewFactory(
kNotificationCustomViewType))
return;
message_center::MessageViewFactory::SetCustomNotificationViewFactory(
kNotificationCustomViewType,
base::BindRepeating(
&PhoneHubNotificationController::CreateCustomNotificationView,
weak_ptr_factory_.GetWeakPtr()));
}
PhoneHubNotificationController::~PhoneHubNotificationController() {
if (manager_)
manager_->RemoveObserver(this);
if (tether_controller_)
tether_controller_->RemoveObserver(this);
}
void PhoneHubNotificationController::SetManager(
chromeos::phonehub::PhoneHubManager* phone_hub_manager) {
chromeos::phonehub::NotificationManager* notification_manager =
phone_hub_manager->GetNotificationManager();
chromeos::phonehub::FeatureStatusProvider* feature_status_provider =
phone_hub_manager->GetFeatureStatusProvider();
chromeos::phonehub::TetherController* tether_controller =
phone_hub_manager->GetTetherController();
phone_model_ = phone_hub_manager->GetPhoneModel();
if (manager_ == notification_manager &&
tether_controller_ == tether_controller &&
feature_status_provider_ == feature_status_provider) {
return;
}
if (manager_)
manager_->RemoveObserver(this);
manager_ = notification_manager;
manager_->AddObserver(this);
if (feature_status_provider_)
feature_status_provider_->RemoveObserver(this);
feature_status_provider_ = feature_status_provider;
feature_status_provider_->AddObserver(this);
if (tether_controller_)
tether_controller_->RemoveObserver(this);
tether_controller_ = tether_controller;
tether_controller_->AddObserver(this);
}
const base::string16 PhoneHubNotificationController::GetPhoneName() const {
if (!phone_model_)
return base::string16();
return phone_model_->phone_name().value_or(base::string16());
}
void PhoneHubNotificationController::OnFeatureStatusChanged() {
DCHECK(feature_status_provider_);
auto status = feature_status_provider_->GetStatus();
// Various states in which the feature is enabled, even if it is not actually
// in use (e.g., if Bluetooth is disabled or if the screen is locked).
bool is_feature_enabled =
status == chromeos::phonehub::FeatureStatus::kUnavailableBluetoothOff ||
status == chromeos::phonehub::FeatureStatus::kLockOrSuspended ||
status == chromeos::phonehub::FeatureStatus::kEnabledButDisconnected ||
status == chromeos::phonehub::FeatureStatus::kEnabledAndConnecting ||
status == chromeos::phonehub::FeatureStatus::kEnabledAndConnected;
// Reset the set of shown notifications when Phone Hub is disabled. If it is
// enabled, we skip this step to ensure that notifications that have already
// been shown do not pop up again and spam the user. See
// https://crbug.com/1157523 for details.
if (!is_feature_enabled)
shown_notification_ids_.clear();
}
void PhoneHubNotificationController::OnNotificationsAdded(
const base::flat_set<int64_t>& notification_ids) {
for (int64_t id : notification_ids) {
SetNotification(manager_->GetNotification(id),
/*is_update=*/false);
}
LogNotificationCount();
}
void PhoneHubNotificationController::OnNotificationsUpdated(
const base::flat_set<int64_t>& notification_ids) {
for (int64_t id : notification_ids) {
SetNotification(manager_->GetNotification(id),
/*is_update=*/true);
}
}
void PhoneHubNotificationController::OnNotificationsRemoved(
const base::flat_set<int64_t>& notification_ids) {
for (int64_t id : notification_ids) {
auto it = notification_map_.find(id);
if (it == notification_map_.end())
continue;
it->second->Remove();
notification_map_.erase(it);
}
LogNotificationCount();
}
void PhoneHubNotificationController::OnAttemptConnectionScanFailed() {
// Add a notification if tether failed.
scoped_refptr<message_center::NotificationDelegate> delegate =
base::MakeRefCounted<message_center::HandleNotificationClickDelegate>(
base::BindRepeating([](base::Optional<int> button_index) {
// When clicked, open Tether Settings page if we can open WebUI
// settings, otherwise do nothing.
if (TrayPopupUtils::CanOpenWebUISettings()) {
Shell::Get()
->system_tray_model()
->client()
->ShowTetherNetworkSettings();
} else {
LOG(WARNING) << "Cannot open Tether Settings since it's not "
"possible to opening WebUI settings";
}
}));
std::unique_ptr<message_center::Notification> notification =
CreateSystemNotification(
message_center::NOTIFICATION_TYPE_SIMPLE,
kPhoneHubInstantTetherNotificationId,
l10n_util::GetStringUTF16(
IDS_ASH_PHONE_HUB_NOTIFICATION_HOTSPOT_FAILED_TITLE),
l10n_util::GetStringUTF16(
IDS_ASH_PHONE_HUB_NOTIFICATION_HOTSPOT_FAILED_MESSAGE),
base::string16() /*display_source */, GURL() /* origin_url */,
message_center::NotifierId(
message_center::NotifierType::SYSTEM_COMPONENT,
kPhoneHubInstantTetherNotificationId),
message_center::RichNotificationData(), std::move(delegate),
kPhoneHubEnableHotspotOnIcon,
message_center::SystemNotificationWarningLevel::NORMAL);
message_center::MessageCenter::Get()->AddNotification(
std::move(notification));
}
void PhoneHubNotificationController::OpenSettings() {
DCHECK(TrayPopupUtils::CanOpenWebUISettings());
Shell::Get()->system_tray_model()->client()->ShowConnectedDevicesSettings();
}
void PhoneHubNotificationController::DismissNotification(
int64_t notification_id) {
CHECK(manager_);
manager_->DismissNotification(notification_id);
phone_hub_metrics::LogNotificationInteraction(
NotificationInteraction::kDismiss);
}
void PhoneHubNotificationController::SendInlineReply(
int64_t notification_id,
const base::string16& inline_reply_text) {
CHECK(manager_);
manager_->SendInlineReply(notification_id, inline_reply_text);
phone_hub_metrics::LogNotificationInteraction(
NotificationInteraction::kInlineReply);
}
void PhoneHubNotificationController::LogNotificationCount() {
int count = notification_map_.size();
phone_hub_metrics::LogNotificationCount(count);
}
void PhoneHubNotificationController::SetNotification(
const chromeos::phonehub::Notification* notification,
bool is_update) {
int64_t phone_hub_id = notification->id();
std::string cros_id = base::StrCat(
{kNotifierId, kNotifierIdSeparator, base::NumberToString(phone_hub_id)});
bool notification_already_exists =
base::Contains(notification_map_, phone_hub_id);
if (!notification_already_exists) {
notification_map_[phone_hub_id] =
std::make_unique<NotificationDelegate>(this, phone_hub_id, cros_id);
}
NotificationDelegate* delegate = notification_map_[phone_hub_id].get();
auto cros_notification =
CreateNotification(notification, cros_id, delegate, is_update);
cros_notification->set_custom_view_type(kNotificationCustomViewType);
shown_notification_ids_.insert(phone_hub_id);
auto* message_center = message_center::MessageCenter::Get();
if (notification_already_exists)
message_center->UpdateNotification(cros_id, std::move(cros_notification));
else
message_center->AddNotification(std::move(cros_notification));
}
std::unique_ptr<message_center::Notification>
PhoneHubNotificationController::CreateNotification(
const chromeos::phonehub::Notification* notification,
const std::string& cros_id,
NotificationDelegate* delegate,
bool is_update) {
message_center::NotifierId notifier_id(
message_center::NotifierType::PHONE_HUB, kNotifierId);
auto notification_type = message_center::NOTIFICATION_TYPE_CUSTOM;
base::string16 title = notification->title().value_or(base::string16());
base::string16 message =
notification->text_content().value_or(base::string16());
auto app_metadata = notification->app_metadata();
base::string16 display_source = app_metadata.visible_app_name;
message_center::RichNotificationData optional_fields;
optional_fields.small_image = app_metadata.icon;
optional_fields.ignore_accent_color_for_small_image = true;
optional_fields.timestamp = notification->timestamp();
auto shared_image = notification->shared_image();
if (shared_image.has_value())
optional_fields.image = shared_image.value();
const gfx::Image& icon = notification->contact_image().value_or(gfx::Image());
optional_fields.priority =
GetSystemPriorityForNotification(notification, is_update);
// If the notification was updated, set renotify to true so that the
// notification pops up again and is visible to the user. See
// https://crbug.com/1159063.
if (is_update)
optional_fields.renotify = true;
message_center::ButtonInfo reply_button;
reply_button.title = l10n_util::GetStringUTF16(
IDS_ASH_PHONE_HUB_NOTIFICATION_INLINE_REPLY_BUTTON);
reply_button.placeholder = base::string16();
optional_fields.buttons.push_back(reply_button);
if (TrayPopupUtils::CanOpenWebUISettings()) {
optional_fields.settings_button_handler =
message_center::SettingsButtonHandler::DELEGATE;
}
return std::make_unique<message_center::Notification>(
notification_type, cros_id, title, message, icon, display_source,
/*origin_url=*/GURL(), notifier_id, optional_fields,
delegate->AsScopedRefPtr());
}
int PhoneHubNotificationController::GetSystemPriorityForNotification(
const chromeos::phonehub::Notification* notification,
bool is_update) {
switch (notification->importance()) {
case chromeos::phonehub::Notification::Importance::kNone:
FALLTHROUGH;
case chromeos::phonehub::Notification::Importance::kMin:
return message_center::MIN_PRIORITY;
case chromeos::phonehub::Notification::Importance::kUnspecified:
FALLTHROUGH;
case chromeos::phonehub::Notification::Importance::kLow:
FALLTHROUGH;
case chromeos::phonehub::Notification::Importance::kDefault:
FALLTHROUGH;
case chromeos::phonehub::Notification::Importance::kHigh:
bool has_notification_been_shown =
base::Contains(shown_notification_ids_, notification->id());
// If the same notification was already shown and has not been updated,
// use LOW_PRIORITY so that the notification is silently added to the
// notification shade. This ensures that we don't spam users with the same
// information multiple times.
if (has_notification_been_shown && !is_update)
return message_center::LOW_PRIORITY;
// Use MAX_PRIORITY, which causes the notification to be shown in a popup
// so that users can see new messages come in as they are chatting. See
// https://crbug.com/1159063.
return message_center::MAX_PRIORITY;
}
}
// static
std::unique_ptr<message_center::MessageView>
PhoneHubNotificationController::CreateCustomNotificationView(
base::WeakPtr<PhoneHubNotificationController> notification_controller,
const message_center::Notification& notification) {
DCHECK_EQ(kNotificationCustomViewType, notification.custom_view_type());
base::string16 phone_name = base::string16();
if (notification_controller)
phone_name = notification_controller->GetPhoneName();
return std::make_unique<PhoneHubNotificationView>(notification, phone_name);
}
} // namespace ash