| // 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 |