// 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/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/notifications/notification_common.h"
#include "chrome/browser/notifications/notification_display_service_impl.h"
#include "chrome/browser/notifications/platform_notification_service_factory.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/browser/safe_browsing/android/notification_content_detection_manager_android.h"
#include "chrome/browser/safe_browsing/notification_content_detection/notification_content_detection_util.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/notifications/notification_constants.h"
#include "chrome/common/notifications/notification_operation.h"
#include "chrome/grit/generated_resources.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/pref_names.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/safe_browsing/content/browser/notification_content_detection/notification_content_detection_constants.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/url_formatter/url_formatter.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/base/l10n/l10n_util.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::ConvertJavaStringToUTF16;
using base::android::ConvertJavaStringToUTF8;
using base::android::ConvertUTF16ToJavaString;
using base::android::ConvertUTF8ToJavaString;
using base::android::JavaRef;
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/41282083
  BUTTON,
  TEXT
};

static 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;
}

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

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

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

  NOTREACHED();
}

}  // 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 std::string& notification_id,
    int32_t java_notification_type,
    const std::string& java_origin_str,
    const std::string& scope_url_str,
    const std::string& profile_id,
    bool incognito,
    const std::string& webapk_package,
    int32_t java_action_index,
    const JavaRef<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 */, std::nullopt /* is_suspicious */,
          base::BindOnce(
              &NotificationPlatformBridgeAndroid::OnNotificationProcessed,
              weak_factory_.GetWeakPtr(), notification_id)));
}

void NotificationPlatformBridgeAndroid::
    StoreCachedWebApkPackageForNotificationId(
        JNIEnv* env,
        const std::string& notification_id,
        const 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 std::string& notification_id,
    int32_t java_notification_type,
    const std::string& origin,
    const std::string& profile_id,
    bool incognito,
    bool 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, std::nullopt /* is_suspicious */,
          base::BindOnce(
              &NotificationPlatformBridgeAndroid::OnNotificationProcessed,
              weak_factory_.GetWeakPtr(), notification_id)));
}

void NotificationPlatformBridgeAndroid::OnNotificationDisablePermission(
    JNIEnv* env,
    const std::string& notification_id,
    int32_t java_notification_type,
    const std::string& origin,
    const std::string& profile_id,
    bool incognito,
    bool is_suspicious) {
  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 */, is_suspicious,
                     base::DoNothing()));
}

void NotificationPlatformBridgeAndroid::SetIsSuspiciousParameterForTesting(
    JNIEnv* env,
    bool is_suspicious) {
  should_use_test_is_suspicious_value_ = true;
  test_is_suspicious_value_ = is_suspicious;
}

void NotificationPlatformBridgeAndroid::OnReportNotificationAsSafe(
    JNIEnv* env,
    const std::string& notification_id,
    const std::string& origin,
    const std::string& profile_id,
    bool incognito) {
  ProfileManager* profile_manager = g_browser_process->profile_manager();
  CHECK(profile_manager);

  profile_manager->LoadProfile(
      GetProfileBaseNameFromProfileId(profile_id), incognito,
      base::BindOnce(&NotificationDisplayServiceImpl::ProfileLoadedCallback,
                     NotificationOperation::kReportAsSafe,
                     NotificationHandler::Type::WEB_PERSISTENT, GURL(origin),
                     notification_id, std::nullopt /* action index */,
                     std::nullopt /* reply */, std::nullopt /* by_user */,
                     std::nullopt /* is_suspicious */, base::DoNothing()));
}

void NotificationPlatformBridgeAndroid::OnReportWarnedNotificationAsSpam(
    JNIEnv* env,
    const std::string& notification_id,
    const std::string& origin,
    const std::string& profile_id,
    bool incognito) {
  ProfileManager* profile_manager = g_browser_process->profile_manager();
  CHECK(profile_manager);

  profile_manager->LoadProfile(
      GetProfileBaseNameFromProfileId(profile_id), incognito,
      base::BindOnce(&NotificationDisplayServiceImpl::ProfileLoadedCallback,
                     NotificationOperation::kReportWarnedAsSpam,
                     NotificationHandler::Type::WEB_PERSISTENT, GURL(origin),
                     notification_id, std::nullopt /* action index */,
                     std::nullopt /* reply */, std::nullopt /* by_user */,
                     std::nullopt /* is_suspicious */, base::DoNothing()));
}

void NotificationPlatformBridgeAndroid::OnReportUnwarnedNotificationAsSpam(
    JNIEnv* env,
    const std::string& notification_id,
    const std::string& origin,
    const std::string& profile_id,
    bool incognito) {
  ProfileManager* profile_manager = g_browser_process->profile_manager();
  CHECK(profile_manager);

  profile_manager->LoadProfile(
      GetProfileBaseNameFromProfileId(profile_id), incognito,
      base::BindOnce(&NotificationDisplayServiceImpl::ProfileLoadedCallback,
                     NotificationOperation::kReportUnwarnedAsSpam,
                     NotificationHandler::Type::WEB_PERSISTENT, GURL(origin),
                     notification_id, std::nullopt /* action index */,
                     std::nullopt /* reply */, std::nullopt /* by_user */,
                     std::nullopt /* is_suspicious */, base::DoNothing()));
}

void NotificationPlatformBridgeAndroid::OnNotificationShowOriginalNotification(
    JNIEnv* env,
    const base::android::JavaRef<jobject>& java_object,
    const std::string& origin,
    const std::string& profile_id,
    bool incognito) {
  ProfileManager* profile_manager = g_browser_process->profile_manager();
  CHECK(profile_manager);

  profile_manager->LoadProfile(
      GetProfileBaseNameFromProfileId(profile_id), incognito,
      base::BindOnce(&NotificationDisplayServiceImpl::ProfileLoadedCallback,
                     NotificationOperation::kShowOriginalNotification,
                     NotificationHandler::Type::WEB_PERSISTENT, GURL(origin),
                     /*notification_id=*/"", std::nullopt /* action index */,
                     std::nullopt /* reply */, std::nullopt /* by_user */,
                     std::nullopt /* is_suspicious */, base::DoNothing()));
}

void NotificationPlatformBridgeAndroid::OnNotificationAlwaysAllowFromOrigin(
    JNIEnv* env,
    const std::string& notification_id,
    const std::string& origin,
    const std::string& profile_id,
    bool incognito) {
  ProfileManager* profile_manager = g_browser_process->profile_manager();
  CHECK(profile_manager);

  const GURL& url = GURL(origin);
  profile_manager->LoadProfile(
      GetProfileBaseNameFromProfileId(profile_id), incognito,
      base::BindOnce(
          &NotificationPlatformBridgeAndroid::AlwaysAllowNotifications,
          weak_factory_.GetWeakPtr(), url, notification_id));
}

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;

  bool skip_ua_buttons = persistent_notification_metadata
                             ? persistent_notification_metadata->skip_ua_buttons
                             : false;
  // Extension notifications never show UA buttons like "Unsubscribe" or
  // "Site Settings".
  if (notification_type == NotificationHandler::Type::EXTENSION ||
      notification_type == NotificationHandler::Type::EXTENSION_REQUEST) {
    skip_ua_buttons = true;
  }

  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();

  int32_t 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(), notification.buttons(),
      should_use_test_is_suspicious_value_
          ? test_is_suspicious_value_
          : (persistent_notification_metadata
                 ? persistent_notification_metadata->is_suspicious
                 : false),
      skip_ua_buttons);

  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();

  GURL scope_url(
      notification_info.service_worker_scope.possibly_invalid_spec());
  std::string scope_url_spec = 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 : "";

  regenerated_notification_infos_.erase(iterator);

  Java_NotificationPlatformBridge_closeNotification(
      env, java_object_, notification_id, scope_url_spec,
      has_queried_webapk_package, 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);
}

void NotificationPlatformBridgeAndroid::OnNotificationProcessed(
    const std::string& notification_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_NotificationPlatformBridge_onNotificationProcessed(env, java_object_,
                                                          notification_id);
}

void NotificationPlatformBridgeAndroid::AlwaysAllowNotifications(
    const GURL& url,
    const std::string& notification_id,
    Profile* profile) {
  // Always allow suspicious notifications from `url`.
  auto* hcsm = HostContentSettingsMapFactory::GetForProfile(profile);
  if (!hcsm) {
    return;
  }
  CHECK(url.is_valid());
  hcsm->SetWebsiteSettingCustomScope(
      ContentSettingsPattern::FromURLNoWildcard(url),
      ContentSettingsPattern::Wildcard(),
      ContentSettingsType::ARE_SUSPICIOUS_NOTIFICATIONS_ALLOWLISTED_BY_USER,
      base::Value(
          base::DictValue().Set(safe_browsing::kIsAllowlistedByUserKey, true)));

  safe_browsing::NotificationContentDetectionUkmUtil::
      RecordSuspiciousNotificationInteractionUkm(
          static_cast<int>(
              safe_browsing::SuspiciousNotificationWarningInteractions::
                  kAlwaysAllow),
          url, notification_id, profile);

  // Send a new notification to tell the user that Chrome will no longer hide
  // notifications from `url`.
  std::u16string notification_title =
      base::FeatureList::IsEnabled(
          safe_browsing::kReportNotificationContentDetectionData)
          ? l10n_util::GetStringFUTF16(
                IDS_CHROME_NO_LONGER_SHOW_WARNINGS_NOTIFICATION_TITLE_NEW,
                url_formatter::FormatUrl(
                    url,
                    url_formatter::kFormatUrlOmitDefaults |
                        url_formatter::kFormatUrlOmitHTTPS |
                        url_formatter::kFormatUrlOmitTrivialSubdomains |
                        url_formatter::kFormatUrlTrimAfterHost,
                    base::UnescapeRule::SPACES, nullptr, nullptr, nullptr))
          : l10n_util::GetStringUTF16(
                IDS_CHROME_NO_LONGER_SHOW_WARNINGS_NOTIFICATION_TITLE);
  std::u16string notification_body =
      base::FeatureList::IsEnabled(
          safe_browsing::kReportNotificationContentDetectionData)
          ? l10n_util::GetStringUTF16(
                IDS_CHROME_NO_LONGER_SHOW_WARNINGS_NOTIFICATION_BODY_NEW)
          : l10n_util::GetStringUTF16(
                IDS_CHROME_NO_LONGER_SHOW_WARNINGS_NOTIFICATION_BODY);
  message_center::Notification notification(
      message_center::NOTIFICATION_TYPE_SIMPLE,
      base::NumberToString(
          PlatformNotificationServiceFactory::GetForProfile(profile)
              ->ReadNextPersistentNotificationId()),
      notification_title, notification_body, ui::ImageModel(), std::u16string(),
      url, message_center::NotifierId(), message_center::RichNotificationData(),
      nullptr);
  // Create new `PersistentNotificationMetadata`, where `is_suspicious` is set
  // to false by default. Set `skip_ua_buttons` to true so the confirmation
  // notification does not restore any UA buttons.
  auto metadata = std::make_unique<PersistentNotificationMetadata>();
  metadata->skip_ua_buttons = true;
  Display(NotificationHandler::Type::WEB_PERSISTENT, profile, notification,
          std::move(metadata));
}

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

NotificationPlatformBridgeAndroid::RegeneratedNotificationInfo::
    RegeneratedNotificationInfo() = default;

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() = default;

base::android::ScopedJavaLocalRef<jobject> ConvertToJavaActionInfo(
    JNIEnv* env,
    const message_center::ButtonInfo& button) {
  std::u16string title = button.title;
  int type = GetNotificationActionType(button);
  std::u16string placeholder;
  if (button.placeholder) {
    placeholder = *button.placeholder;
  }
  ScopedJavaLocalRef<jobject> icon =
      JNI_NotificationPlatformBridge_ConvertToJavaBitmap(env, button.icon);
  return Java_ActionInfo_createActionInfo(env, title, icon, type, placeholder);
}

DEFINE_JNI(ActionInfo)
DEFINE_JNI(NotificationPlatformBridge)
