blob: 5f32b6bb6ec12e127e0d632952e2b9ed2c74b8c4 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromeos/ash/components/phonehub/notification_processor.h"
#include "ash/constants/ash_features.h"
#include "base/barrier_closure.h"
#include "base/functional/callback.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/phonehub/notification.h"
#include "chromeos/ash/components/phonehub/notification_manager.h"
#include "services/data_decoder/public/cpp/decode_image.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/image/image_skia.h"
namespace ash {
namespace phonehub {
namespace {
// Constants to override the Messages app monochrome icon color.
const char kMessagesPackageName[] = "com.google.android.apps.messaging";
const SkColor kMessagesOverrideColor = gfx::kGoogleBlue600;
absl::optional<SkColor> getMonochromeIconColor(const proto::Notification& proto,
const gfx::Image& icon) {
if (icon.IsEmpty() || !proto.origin_app().has_icon_color()) {
return absl::nullopt;
}
if (proto.origin_app().package_name() == kMessagesPackageName) {
// The notification color supplied by the Messages app (Bugle) is based
// on light/dark mode of the phone, not the Chromebook, with no way to
// query for both at runtime. These constants are used to override with
// a fixed color. See conversation at b/207089786 for more details.
return kMessagesOverrideColor;
}
return SkColorSetRGB(proto.origin_app().icon_color().red(),
proto.origin_app().icon_color().green(),
proto.origin_app().icon_color().blue());
}
Notification::Importance GetNotificationImportanceFromProto(
proto::NotificationImportance importance) {
switch (importance) {
case proto::NotificationImportance::UNSPECIFIED:
return Notification::Importance::kUnspecified;
case proto::NotificationImportance::NONE:
return Notification::Importance::kNone;
case proto::NotificationImportance::MIN:
return Notification::Importance::kMin;
case proto::NotificationImportance::LOW:
return Notification::Importance::kLow;
case proto::NotificationImportance::DEFAULT:
return Notification::Importance::kDefault;
case proto::NotificationImportance::HIGH:
return Notification::Importance::kHigh;
default:
return Notification::Importance::kUnspecified;
}
}
bool HasSupportedActionIdInProto(const proto::Notification& proto) {
for (const auto& action : proto.actions()) {
if (action.type() == proto::Action_InputType::Action_InputType_TEXT ||
action.call_action() ==
proto::Action_CallAction::Action_CallAction_ANSWER ||
action.call_action() ==
proto::Action_CallAction::Action_CallAction_DECLINE ||
action.call_action() ==
proto::Action_CallAction::Action_CallAction_HANGUP)
return true;
}
return false;
}
Notification CreateInternalNotification(const proto::Notification& proto,
const gfx::Image& icon,
const gfx::Image& shared_image,
const gfx::Image& contact_image) {
base::flat_map<Notification::ActionType, int64_t> action_id_map;
Notification::InteractionBehavior behavior =
Notification::InteractionBehavior::kNone;
for (const auto& action : proto.actions()) {
if (action.type() == proto::Action_InputType::Action_InputType_TEXT) {
action_id_map[Notification::ActionType::kInlineReply] = action.id();
} else if (action.type() ==
proto::Action_InputType::Action_InputType_OPEN) {
behavior = Notification::InteractionBehavior::kOpenable;
} else if (action.call_action() ==
proto::Action_CallAction::Action_CallAction_ANSWER) {
action_id_map[Notification::ActionType::kAnswer] = action.id();
} else if (action.call_action() ==
proto::Action_CallAction::Action_CallAction_DECLINE) {
action_id_map[Notification::ActionType::kDecline] = action.id();
} else if (action.call_action() ==
proto::Action_CallAction::Action_CallAction_HANGUP) {
action_id_map[Notification::ActionType::kHangup] = action.id();
}
}
Notification::Category category = Notification::Category::kNone;
switch (proto.category()) {
case proto::Notification::Category::Notification_Category_UNSPECIFIED:
category = Notification::Category::kNone;
break;
case proto::Notification::Category::Notification_Category_CONVERSATION:
category = Notification::Category::kConversation;
break;
case proto::Notification::Category::Notification_Category_INCOMING_CALL:
category = Notification::Category::kIncomingCall;
break;
case proto::Notification::Category::Notification_Category_ONGOING_CALL:
category = Notification::Category::kOngoingCall;
break;
case proto::Notification::Category::Notification_Category_SCREEN_CALL:
category = Notification::Category::kScreenCall;
break;
case proto::
Notification_Category_Notification_Category_INT_MIN_SENTINEL_DO_NOT_USE_: // NOLINT
case proto::
Notification_Category_Notification_Category_INT_MAX_SENTINEL_DO_NOT_USE_: // NOLINT
PA_LOG(WARNING) << "Notification category is unknown or unspecified.";
break;
}
absl::optional<std::u16string> title = absl::nullopt;
if (!proto.title().empty())
title = base::UTF8ToUTF16(proto.title());
absl::optional<std::u16string> text_content = absl::nullopt;
if (!proto.text_content().empty())
text_content = base::UTF8ToUTF16(proto.text_content());
absl::optional<gfx::Image> opt_shared_image = absl::nullopt;
if (!shared_image.IsEmpty())
opt_shared_image = shared_image;
absl::optional<gfx::Image> opt_contact_image = absl::nullopt;
if (!contact_image.IsEmpty())
opt_contact_image = contact_image;
bool icon_is_monochrome =
proto.origin_app().icon_styling() ==
proto::NotificationIconStyling::ICON_STYLE_MONOCHROME_SMALL_ICON;
absl::optional<SkColor> icon_color =
icon_is_monochrome ? getMonochromeIconColor(proto, icon) : absl::nullopt;
return Notification(proto.id(),
Notification::AppMetadata(
base::UTF8ToUTF16(proto.origin_app().visible_name()),
proto.origin_app().package_name(), icon, icon_color,
icon_is_monochrome, proto.origin_app().user_id(),
proto.origin_app().app_streamability_status()),
base::Time::FromJsTime(proto.epoch_time_millis()),
GetNotificationImportanceFromProto(proto.importance()),
category, action_id_map, behavior, title, text_content,
opt_shared_image, opt_contact_image);
}
} // namespace
NotificationProcessor::DecodeImageRequestMetadata::DecodeImageRequestMetadata(
int64_t notification_id,
NotificationImageField image_field,
const std::string& data)
: notification_id(notification_id), image_field(image_field), data(data) {}
NotificationProcessor::NotificationProcessor(
NotificationManager* notification_manager)
: NotificationProcessor(notification_manager,
std::make_unique<ImageDecoderDelegate>()) {}
NotificationProcessor::NotificationProcessor(
NotificationManager* notification_manager,
std::unique_ptr<ImageDecoderDelegate> delegate)
: notification_manager_(notification_manager),
delegate_(std::move(delegate)) {}
NotificationProcessor::~NotificationProcessor() {}
void NotificationProcessor::ClearNotificationsAndPendingUpdates() {
notification_manager_->ClearNotificationsInternal();
// Clear pending updates that may occur.
weak_ptr_factory_.InvalidateWeakPtrs();
pending_notification_requests_ = base::queue<base::OnceClosure>();
id_to_images_map_.clear();
}
void NotificationProcessor::AddNotifications(
const std::vector<proto::Notification>& notification_protos) {
if (notification_protos.empty())
return;
std::vector<proto::Notification> processed_notification_protos;
std::vector<DecodeImageRequestMetadata> decode_image_requests;
for (const auto& proto : notification_protos) {
// Only process notifications that are messaging apps with inline-replies
// or dialer apps with call-style actions.
if (!HasSupportedActionIdInProto(proto))
continue;
processed_notification_protos.emplace_back(proto);
decode_image_requests.emplace_back(
proto.id(), NotificationImageField::kIcon, proto.origin_app().icon());
if (!proto.shared_image().empty()) {
decode_image_requests.emplace_back(proto.id(),
NotificationImageField::kSharedImage,
proto.shared_image());
}
if (!proto.contact_image().empty()) {
decode_image_requests.emplace_back(proto.id(),
NotificationImageField::kContactImage,
proto.contact_image());
}
}
if (decode_image_requests.empty()) {
PA_LOG(INFO) << "Cannot find any image to decode for the notifications";
return;
}
base::RepeatingClosure barrier = base::BarrierClosure(
decode_image_requests.size(),
base::BindOnce(&NotificationProcessor::OnAllImagesDecoded,
weak_ptr_factory_.GetWeakPtr(),
std::move(processed_notification_protos)));
base::OnceClosure add_request =
base::BindOnce(&NotificationProcessor::StartDecodingImages,
weak_ptr_factory_.GetWeakPtr(),
std::move(decode_image_requests), barrier);
pending_notification_requests_.emplace(std::move(add_request));
ProcessRequestQueue();
}
void NotificationProcessor::RemoveNotifications(
const base::flat_set<int64_t>& notification_ids) {
if (notification_ids.empty())
return;
base::flat_set<int64_t> removed_notification_ids;
for (const int64_t& id : notification_ids) {
removed_notification_ids.emplace(id);
}
base::OnceClosure remove_request = base::BindOnce(
&NotificationProcessor::RemoveNotificationsAndProcessNextRequest,
weak_ptr_factory_.GetWeakPtr(), std::move(removed_notification_ids));
pending_notification_requests_.emplace(std::move(remove_request));
ProcessRequestQueue();
}
void NotificationProcessor::StartDecodingImages(
const std::vector<DecodeImageRequestMetadata>& decode_image_requests,
base::RepeatingClosure done_closure) {
DCHECK(!decode_image_requests.empty());
DCHECK(!done_closure.is_null());
id_to_images_map_.clear();
for (const auto& request : decode_image_requests) {
delegate_->PerformImageDecode(
request.data,
base::BindOnce(&NotificationProcessor::OnDecodedBitmapReady,
weak_ptr_factory_.GetWeakPtr(), request, done_closure));
}
}
void NotificationProcessor::ImageDecoderDelegate::PerformImageDecode(
const std::string& data,
DecodeImageCallback single_image_decoded_closure) {
data_decoder::DecodeImage(
&data_decoder_, base::as_bytes(base::make_span(data)),
data_decoder::mojom::ImageCodec::kDefault,
/*shrink_to_fit=*/true, data_decoder::kDefaultMaxSizeInBytes,
/*desired_image_frame_size=*/gfx::Size(),
std::move(single_image_decoded_closure));
}
void NotificationProcessor::OnDecodedBitmapReady(
const DecodeImageRequestMetadata& request,
base::OnceClosure done_closure,
const SkBitmap& decoded_bitmap) {
gfx::ImageSkia image_skia =
gfx::ImageSkia::CreateFrom1xBitmap(decoded_bitmap);
// If |image_skia| is null, indicating that the data decoder failed to decode
// the image, the image will be empty, and cannot be made thread safe.
if (!image_skia.isNull())
image_skia.MakeThreadSafe();
auto it = id_to_images_map_.find(request.notification_id);
if (it == id_to_images_map_.end()) {
it = id_to_images_map_.insert(
it, std::pair<int64_t, NotificationImages>(request.notification_id,
NotificationImages()));
}
switch (request.image_field) {
case NotificationImageField::kIcon:
it->second.icon = gfx::Image(image_skia);
break;
case NotificationImageField::kSharedImage:
it->second.shared_image = gfx::Image(image_skia);
break;
case NotificationImageField::kContactImage:
it->second.contact_image = gfx::Image(image_skia);
break;
}
std::move(done_closure).Run();
}
void NotificationProcessor::OnAllImagesDecoded(
std::vector<proto::Notification> notification_protos) {
base::flat_set<Notification> notifications;
for (const auto& proto : notification_protos) {
auto it = id_to_images_map_.find(proto.id());
if (it == id_to_images_map_.end())
continue;
NotificationImages notification_images = it->second;
notifications.emplace(CreateInternalNotification(
proto, notification_images.icon, notification_images.shared_image,
notification_images.contact_image));
}
AddNotificationsAndProcessNextRequest(notifications);
}
void NotificationProcessor::ProcessRequestQueue() {
if (pending_notification_requests_.empty())
return;
// Processing the latest request has not been completed.
if (pending_notification_requests_.front().is_null())
return;
std::move(pending_notification_requests_.front()).Run();
}
void NotificationProcessor::CompleteRequest() {
DCHECK(!pending_notification_requests_.empty());
DCHECK(pending_notification_requests_.front().is_null());
pending_notification_requests_.pop();
ProcessRequestQueue();
}
void NotificationProcessor::AddNotificationsAndProcessNextRequest(
const base::flat_set<Notification>& notifications) {
notification_manager_->SetNotificationsInternal(notifications);
CompleteRequest();
}
void NotificationProcessor::RemoveNotificationsAndProcessNextRequest(
base::flat_set<int64_t> removed_notification_ids) {
notification_manager_->RemoveNotificationsInternal(removed_notification_ids);
CompleteRequest();
}
} // namespace phonehub
} // namespace ash