chromium/chrome/browser/notifications/notification_platform_bridge_android.cc

// Copyright 2014 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/notifications/notification_platform_bridge_android.h"

#include <memory>
#include <utility>
#include <vector>

#include "base/android/jni_android.h"
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/check.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/notifications/notification_common.h"
#include "chrome/browser/notifications/notification_display_service_impl.h"
#include "chrome/browser/notifications/platform_notification_service_impl.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/notifications/notification_constants.h"
#include "chrome/common/notifications/notification_operation.h"
#include "components/content_settings/core/common/pref_names.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/persistent_notification_status.h"
#include "third_party/blink/public/common/notifications/platform_notification_data.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/color/color_provider_manager.h"
#include "ui/gfx/android/java_bitmap.h"
#include "ui/gfx/image/image.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/native_theme/native_theme.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chrome/android/chrome_jni_headers/ActionInfo_jni.h"
#include "chrome/android/chrome_jni_headers/NotificationPlatformBridge_jni.h"

using base::android::AttachCurrentThread;
using base::android::ConvertJavaStringToUTF8;
using base::android::ConvertJavaStringToUTF16;
using base::android::ConvertUTF16ToJavaString;
using base::android::ConvertUTF8ToJavaString;
using base::android::JavaParamRef;
using base::android::ScopedJavaLocalRef;

namespace {

// A Java counterpart will be generated for this enum.
// GENERATED_JAVA_ENUM_PACKAGE: org.chromium.chrome.browser.notifications
enum NotificationActionType {
  // NB. Making this a one-line enum breaks code generation! crbug.com/657847
  BUTTON,
  TEXT
};

ScopedJavaLocalRef<jobject> JNI_NotificationPlatformBridge_ConvertToJavaBitmap(
    JNIEnv* env,
    const gfx::Image& icon) {
  SkBitmap skbitmap = icon.AsBitmap();
  ScopedJavaLocalRef<jobject> j_bitmap;
  if (!skbitmap.drawsNothing())
    j_bitmap = gfx::ConvertToJavaBitmap(skbitmap);
  return j_bitmap;
}

NotificationActionType GetNotificationActionType(
    message_center::ButtonInfo button) {
  return button.placeholder ? NotificationActionType::TEXT
                            : NotificationActionType::BUTTON;
}

ScopedJavaLocalRef<jobjectArray> ConvertToJavaActionInfos(
    const std::vector<message_center::ButtonInfo>& buttons) {
  JNIEnv* env = AttachCurrentThread();
  ScopedJavaLocalRef<jclass> clazz = base::android::GetClass(
      env, "org/chromium/chrome/browser/notifications/ActionInfo");
  jobjectArray actions = env->NewObjectArray(buttons.size(), clazz.obj(),
                                             nullptr /* initialElement */);
  base::android::CheckException(env);

  for (size_t i = 0; i < buttons.size(); ++i) {
    const auto& button = buttons[i];
    ScopedJavaLocalRef<jstring> title =
        base::android::ConvertUTF16ToJavaString(env, button.title);
    int type = GetNotificationActionType(button);
    ScopedJavaLocalRef<jstring> placeholder;
    if (button.placeholder) {
      placeholder =
          base::android::ConvertUTF16ToJavaString(env, *button.placeholder);
    }
    ScopedJavaLocalRef<jobject> icon =
        JNI_NotificationPlatformBridge_ConvertToJavaBitmap(env, button.icon);
    ScopedJavaLocalRef<jobject> action_info = Java_ActionInfo_createActionInfo(
        AttachCurrentThread(), title, icon, type, placeholder);
    env->SetObjectArrayElement(actions, i, action_info.obj());
  }

  return ScopedJavaLocalRef<jobjectArray>(env, actions);
}

constexpr jint NotificationTypeToJava(
    NotificationHandler::Type notification_type) {
  return static_cast<jint>(notification_type);
}

constexpr NotificationHandler::Type JavaToNotificationType(
    jint notification_type) {
  constexpr jint kMinValue =
      NotificationTypeToJava(NotificationHandler::Type::WEB_PERSISTENT);
  constexpr jint kMaxValue =
      NotificationTypeToJava(NotificationHandler::Type::MAX);

  if (notification_type >= kMinValue && notification_type <= kMaxValue)
    return static_cast<NotificationHandler::Type>(notification_type);

  NOTREACHED_IN_MIGRATION();
  return NotificationHandler::Type::WEB_PERSISTENT;
}

}  // namespace

// Called by the Java side when a notification event has been received, but the
// NotificationBridge has not been initialized yet. Enforce initialization of
// the class.
static void JNI_NotificationPlatformBridge_InitializeNotificationPlatformBridge(
    JNIEnv* env) {
  g_browser_process->notification_platform_bridge();
}

// static
std::unique_ptr<NotificationPlatformBridge>
NotificationPlatformBridge::Create() {
  return std::make_unique<NotificationPlatformBridgeAndroid>();
}

// static
bool NotificationPlatformBridge::CanHandleType(
    NotificationHandler::Type notification_type) {
  return notification_type != NotificationHandler::Type::TRANSIENT;
}

NotificationPlatformBridgeAndroid::NotificationPlatformBridgeAndroid() {
  java_object_.Reset(Java_NotificationPlatformBridge_create(
      AttachCurrentThread(), reinterpret_cast<intptr_t>(this)));
}

NotificationPlatformBridgeAndroid::~NotificationPlatformBridgeAndroid() {
  Java_NotificationPlatformBridge_destroy(AttachCurrentThread(), java_object_);
}

void NotificationPlatformBridgeAndroid::OnNotificationClicked(
    JNIEnv* env,
    const JavaParamRef<jobject>& java_object,
    std::string& notification_id,
    jint java_notification_type,
    std::string& java_origin_str,
    std::string& scope_url_str,
    std::string& profile_id,
    jboolean incognito,
    std::string& webapk_package,
    jint java_action_index,
    const JavaParamRef<jstring>& java_reply) {
  std::optional<std::u16string> reply;
  if (java_reply)
    reply = ConvertJavaStringToUTF16(env, java_reply);

  GURL origin(java_origin_str);
  GURL scope_url(scope_url_str);
  regenerated_notification_infos_[notification_id] =
      RegeneratedNotificationInfo(scope_url, webapk_package);

  std::optional<int> action_index;
  if (java_action_index != kNotificationInvalidButtonIndex)
    action_index = java_action_index;

  ProfileManager* profile_manager = g_browser_process->profile_manager();
  DCHECK(profile_manager);

  NotificationHandler::Type notification_type =
      JavaToNotificationType(java_notification_type);

  profile_manager->LoadProfile(
      GetProfileBaseNameFromProfileId(profile_id), incognito,
      base::BindOnce(&NotificationDisplayServiceImpl::ProfileLoadedCallback,
                     NotificationOperation::kClick, notification_type, origin,
                     notification_id, std::move(action_index), std::move(reply),
                     std::nullopt /* by_user */));
}

void NotificationPlatformBridgeAndroid::
    StoreCachedWebApkPackageForNotificationId(
        JNIEnv* env,
        const base::android::JavaParamRef<jobject>& java_object,
        std::string& notification_id,
        std::string& webapk_package) {
  const auto iterator = regenerated_notification_infos_.find(notification_id);
  if (iterator == regenerated_notification_infos_.end())
    return;

  const RegeneratedNotificationInfo& info = iterator->second;
  regenerated_notification_infos_[notification_id] =
      RegeneratedNotificationInfo(info.service_worker_scope, webapk_package);
}

void NotificationPlatformBridgeAndroid::OnNotificationClosed(
    JNIEnv* env,
    const JavaParamRef<jobject>& java_object,
    std::string& notification_id,
    jint java_notification_type,
    std::string& origin,
    std::string& profile_id,
    jboolean incognito,
    jboolean by_user) {
  // The notification was closed by the platform, so clear all local state.
  regenerated_notification_infos_.erase(notification_id);

  ProfileManager* profile_manager = g_browser_process->profile_manager();
  DCHECK(profile_manager);

  NotificationHandler::Type notification_type =
      JavaToNotificationType(java_notification_type);

  profile_manager->LoadProfile(
      GetProfileBaseNameFromProfileId(profile_id), incognito,
      base::BindOnce(&NotificationDisplayServiceImpl::ProfileLoadedCallback,
                     NotificationOperation::kClose, notification_type,
                     GURL(origin), notification_id,
                     std::nullopt /* action index */, std::nullopt /* reply */,
                     by_user));
}

void NotificationPlatformBridgeAndroid::OnNotificationDisablePermission(
    JNIEnv* env,
    const JavaParamRef<jobject>& java_object,
    std::string& notification_id,
    jint java_notification_type,
    std::string& origin,
    std::string& profile_id,
    jboolean incognito) {
  ProfileManager* profile_manager = g_browser_process->profile_manager();
  DCHECK(profile_manager);

  NotificationHandler::Type notification_type =
      JavaToNotificationType(java_notification_type);

  profile_manager->LoadProfile(
      GetProfileBaseNameFromProfileId(profile_id), incognito,
      base::BindOnce(&NotificationDisplayServiceImpl::ProfileLoadedCallback,
                     NotificationOperation::kDisablePermission,
                     notification_type, GURL(origin), notification_id,
                     std::nullopt /* action index */, std::nullopt /* reply */,
                     std::nullopt /* by_user */));
}

void NotificationPlatformBridgeAndroid::Display(
    NotificationHandler::Type notification_type,
    Profile* profile,
    const message_center::Notification& notification,
    std::unique_ptr<NotificationCommon::Metadata> metadata) {
  DCHECK(CanHandleType(notification_type));

  JNIEnv* env = AttachCurrentThread();

  GURL origin_url(notification.origin_url().DeprecatedGetOriginAsURL());

  // TODO(peter): Reconsider the meta-data system to try to remove this branch.
  const PersistentNotificationMetadata* persistent_notification_metadata =
      PersistentNotificationMetadata::From(metadata.get());

  GURL scope_url = persistent_notification_metadata
                       ? persistent_notification_metadata->service_worker_scope
                       : origin_url;
  if (!scope_url.is_valid())
    scope_url = origin_url;

  ScopedJavaLocalRef<jobject> android_profile = profile->GetJavaObject();

  SkBitmap image_bitmap = notification.image().AsBitmap();

  const auto* const color_provider =
      ui::ColorProviderManager::Get().GetColorProviderFor(
          ui::NativeTheme::GetInstanceForWeb()->GetColorProviderKey(nullptr));
  const SkBitmap* notification_icon_bitmap =
      notification.icon().Rasterize(color_provider).bitmap();

  SkBitmap badge_bitmap = notification.small_image().AsBitmap();

  ScopedJavaLocalRef<jobjectArray> actions =
      ConvertToJavaActionInfos(notification.buttons());

  jint j_notification_type = NotificationTypeToJava(notification_type);

  Java_NotificationPlatformBridge_displayNotification(
      env, java_object_, notification.id(), j_notification_type,
      origin_url.spec(), scope_url.spec(), GetProfileId(profile),
      android_profile, notification.title(), notification.message(),
      image_bitmap, *notification_icon_bitmap, badge_bitmap,
      notification.vibration_pattern(),
      notification.timestamp().InMillisecondsSinceUnixEpoch(),
      notification.renotify(), notification.silent(), actions);

  regenerated_notification_infos_[notification.id()] =
      RegeneratedNotificationInfo(scope_url, std::nullopt);
}

void NotificationPlatformBridgeAndroid::Close(
    Profile* profile,
    const std::string& notification_id) {
  const auto iterator = regenerated_notification_infos_.find(notification_id);
  if (iterator == regenerated_notification_infos_.end())
    return;

  const RegeneratedNotificationInfo& notification_info = iterator->second;

  JNIEnv* env = AttachCurrentThread();

  ScopedJavaLocalRef<jstring> j_notification_id =
      ConvertUTF8ToJavaString(env, notification_id);

  GURL scope_url(
      notification_info.service_worker_scope.possibly_invalid_spec());
  ScopedJavaLocalRef<jstring> j_scope_url =
      ConvertUTF8ToJavaString(env, scope_url.spec());

  bool has_queried_webapk_package =
      notification_info.webapk_package.has_value();
  std::string webapk_package =
      has_queried_webapk_package ? *notification_info.webapk_package : "";
  ScopedJavaLocalRef<jstring> j_webapk_package =
      ConvertUTF8ToJavaString(env, webapk_package);

  regenerated_notification_infos_.erase(iterator);

  Java_NotificationPlatformBridge_closeNotification(
      env, java_object_, j_notification_id, j_scope_url,
      has_queried_webapk_package, j_webapk_package);
}

void NotificationPlatformBridgeAndroid::DisplayServiceShutDown(
    Profile* profile) {}

void NotificationPlatformBridgeAndroid::GetDisplayed(
    Profile* profile,
    GetDisplayedNotificationsCallback callback) const {
  std::set<std::string> displayed_notifications;
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(callback), std::move(displayed_notifications),
                     false /* supports_synchronization */));
}

void NotificationPlatformBridgeAndroid::GetDisplayedForOrigin(
    Profile* profile,
    const GURL& origin,
    GetDisplayedNotificationsCallback callback) const {
  std::set<std::string> displayed_notifications;
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(callback), std::move(displayed_notifications),
                     false /* supports_synchronization */));
}

void NotificationPlatformBridgeAndroid::SetReadyCallback(
    NotificationBridgeReadyCallback callback) {
  std::move(callback).Run(true);
}

// static
void NotificationPlatformBridgeAndroid::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterBooleanPref(prefs::kNotificationsVibrateEnabled, true);
}

NotificationPlatformBridgeAndroid::RegeneratedNotificationInfo::
    RegeneratedNotificationInfo() {}

NotificationPlatformBridgeAndroid::RegeneratedNotificationInfo::
    RegeneratedNotificationInfo(
        const GURL& service_worker_scope,
        const std::optional<std::string>& webapk_package)
    : service_worker_scope(service_worker_scope),
      webapk_package(webapk_package) {}

NotificationPlatformBridgeAndroid::RegeneratedNotificationInfo::
    ~RegeneratedNotificationInfo() {}