chromium/chromeos/ash/components/phonehub/notification_processor.cc

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

std::optional<SkColor> getMonochromeIconColor(const proto::Notification& proto,
                                              const gfx::Image& icon) {
  if (icon.IsEmpty() || !proto.origin_app().has_monochrome_icon_color()) {
    return std::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().monochrome_icon_color().red(),
                       proto.origin_app().monochrome_icon_color().green(),
                       proto.origin_app().monochrome_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;
}

bool IsMonochromeIconEnabled(const proto::Notification& notification_proto) {
  return features::IsPhoneHubMonochromeNotificationIconsEnabled() &&
         notification_proto.origin_app().has_monochrome_icon_mask();
}

Notification CreateInternalNotification(const proto::Notification& proto,
                                        const gfx::Image& color_icon,
                                        const gfx::Image& monochrome_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;
  }

  std::optional<std::u16string> title = std::nullopt;
  if (!proto.title().empty())
    title = base::UTF8ToUTF16(proto.title());

  std::optional<std::u16string> text_content = std::nullopt;
  if (!proto.text_content().empty())
    text_content = base::UTF8ToUTF16(proto.text_content());

  std::optional<gfx::Image> opt_shared_image = std::nullopt;
  if (!shared_image.IsEmpty())
    opt_shared_image = shared_image;

  std::optional<gfx::Image> opt_contact_image = std::nullopt;
  if (!contact_image.IsEmpty())
    opt_contact_image = contact_image;

  bool icon_is_monochrome = IsMonochromeIconEnabled(proto);
  std::optional<SkColor> icon_color =
      icon_is_monochrome ? getMonochromeIconColor(proto, monochrome_icon)
                         : std::nullopt;

  return Notification(
      proto.id(),
      Notification::AppMetadata(
          base::UTF8ToUTF16(proto.origin_app().visible_name()),
          proto.origin_app().package_name(), color_icon, monochrome_icon,
          icon_color, icon_is_monochrome, proto.origin_app().user_id(),
          proto.origin_app().app_streamability_status()),
      base::Time::FromMillisecondsSinceUnixEpoch(proto.epoch_time_millis()),
      GetNotificationImportanceFromProto(proto.importance()), category,
      action_id_map, behavior, title, text_content, opt_shared_image,
      opt_contact_image);
}

}  // namespace

NotificationProcessor::NotificationImages::NotificationImages() = default;

NotificationProcessor::NotificationImages::~NotificationImages() = default;

NotificationProcessor::NotificationImages::NotificationImages(
    const NotificationImages& other) = default;

NotificationProcessor::NotificationImages&
NotificationProcessor::NotificationImages::operator=(
    const NotificationImages& other) = default;

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::kColorIcon,
                                       proto.origin_app().icon());

    if (IsMonochromeIconEnabled(proto)) {
      decode_image_requests.emplace_back(
          proto.id(), NotificationImageField::kMonochromeIcon,
          proto.origin_app().monochrome_icon_mask());
    } else {
      decode_image_requests.emplace_back(
          proto.id(), NotificationImageField::kMonochromeIcon,
          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::kColorIcon:
      it->second.color_icon = gfx::Image(image_skia);
      break;
    case NotificationImageField::kMonochromeIcon:
      it->second.monochrome_icon_mask = 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.color_icon,
        notification_images.monochrome_icon_mask,
        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