chromium/chrome/browser/download/download_status_updater_lacros.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "base/check.h"
#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/supports_user_data.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/download/bubble/download_bubble_prefs.h"
#include "chrome/browser/download/bubble/download_bubble_ui_controller.h"
#include "chrome/browser/download/download_commands.h"
#include "chrome/browser/download/download_item_model.h"
#include "chrome/browser/download/download_item_warning_data.h"
#include "chrome/browser/download/download_status_updater.h"
#include "chrome/browser/download/download_ui_model.h"
#include "chrome/browser/download/download_ui_safe_browsing_util.h"
#include "chrome/browser/download/offline_item_utils.h"
#include "chrome/browser/image_decoder/image_decoder.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/profiles/keep_alive/scoped_profile_keep_alive.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_window.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/download/download_bubble_info_utils.h"
#include "chrome/browser/ui/download/download_bubble_row_view_info.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/crosapi/mojom/download_controller.mojom.h"
#include "chromeos/crosapi/mojom/download_status_updater.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "components/download/content/public/all_download_item_notifier.h"
#include "components/download/public/common/download_item_utils.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/download_item_utils.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_key.h"
#include "ui/color/color_provider_manager.h"
#include "ui/display/types/display_constants.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/native_theme/native_theme.h"

namespace {

// Constants -------------------------------------------------------------------

// The DIP size of the rasterized icon. Ensures that the icon is large enough
// for download status clients to resize with sufficient resolution.
constexpr int kIconSize = 50;

// The key referring to an image decoder task.
constexpr char kImageDecoderTaskKey[] = "kImageDecoderTask";

// Images larger than this threshold should not be decoded.
constexpr size_t kImageDecoderTaskMaxFileSize = 10 * 1024 * 1024;  // 10 MB

// Helpers ---------------------------------------------------------------------

// Returns the corresponding color of `id` under the specific `color_mode`.
// WARNING: Sending UI icons directly has drawbacks (see http://b/328070365).
// Prefer sending metadata to construct the UI instead.
SkColor GetColor(ui::ColorId id, ui::ColorProviderKey::ColorMode color_mode) {
  ui::ColorProviderKey provider_key =
      ui::NativeTheme::GetInstanceForNativeUi()->GetColorProviderKey(
          /*custom_theme=*/nullptr);
  provider_key.color_mode = color_mode;
  return ui::ColorProviderManager::Get()
      .GetColorProviderFor(provider_key)
      ->GetColor(id);
}

crosapi::mojom::DownloadStatusUpdater* GetRemote(
    std::optional<uint32_t> min_version = std::nullopt) {
  using DownloadStatusUpdater = crosapi::mojom::DownloadStatusUpdater;
  auto* service = chromeos::LacrosService::Get();
  if (!service || !service->IsAvailable<DownloadStatusUpdater>()) {
    return nullptr;
  }
  // NOTE: Use `remote.version()` rather than `service->GetInterfaceVersion()`
  // as the latter does not respect versions of remotes injected for testing.
  auto& remote = service->GetRemote<DownloadStatusUpdater>();
  return remote.version() >= min_version.value_or(remote.version())
             ? remote.get()
             : nullptr;
}

bool IsCommandEnabled(
    const std::vector<DownloadBubbleQuickAction>& quick_actions,
    DownloadCommands::Command command) {
  // To support other commands, we may need to update checks below to also
  // inspect `DownloadBubbleSecurityViewInfo` subpage buttons.
  CHECK(command == DownloadCommands::CANCEL ||
        command == DownloadCommands::PAUSE ||
        command == DownloadCommands::RESUME);

  // A command is enabled if the `DownloadBubbleRowViewInfo` contains
  // a quick action for it. This is preferred over
  // non-`DownloadBubbleRowViewInfo`-based determination of command
  // enablement as it takes more signals into account, e.g. if the
  // download has been marked dangerous.
  return base::Contains(quick_actions, command,
                        &DownloadBubbleQuickAction::command);
}

// Reads a specified image into binary data. Returns an empty string if
// unsuccessful. NOTE:
// 1. This function should be called only when the file size is not greater than
//    `kImageDecoderTaskMaxFileSize`.
// 2. This function is blocking so it should not be called from the UI thread.
std::string ReadImage(const base::FilePath& file_path) {
  CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));

  std::string data;
  if (!base::ReadFileToString(file_path, &data)) {
    return std::string();
  }

  if (data.size() > kImageDecoderTaskMaxFileSize) {
    data.clear();
    LOG(ERROR) << "Attempted to read a too large image file.";
  }

  return data;
}

// ImageDecoderTask ------------------------------------------------------------

// Represents an async task to decode a download image. Has two stages:
// 1. Load the image's binary data.
// 2. Decode the binary data into a `gfx::ImageSkia`.
class ImageDecoderTask : public base::SupportsUserData::Data,
                         public ImageDecoder::ImageRequest {
 public:
  void Run(const base::FilePath& image_path,
           base::OnceClosure task_success_callback) {
    CHECK(!task_success_callback_);
    CHECK(task_success_callback);
    task_success_callback_ = std::move(task_success_callback);

    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE,
        /*traits=*/{base::MayBlock(), base::TaskPriority::BEST_EFFORT},
        base::BindOnce(&ReadImage, image_path),
        base::BindOnce(&ImageDecoderTask::OnImageLoaded,
                       weak_ptr_factory_.GetWeakPtr()));
  }

  const gfx::ImageSkia& image() const { return image_; }

 private:
  // ImageDecoder::ImageRequest:
  void OnImageDecoded(const SkBitmap& decoded_image) override {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

    if (!decoded_image.drawsNothing()) {
      image_ = gfx::ImageSkia::CreateFrom1xBitmap(decoded_image);
      std::move(task_success_callback_).Run();
    }
  }

  void OnImageLoaded(std::string image_data) {
    if (!image_data.empty()) {
      ImageDecoder::Start(/*image_request=*/this, std::move(image_data));
    }
  }

  // Called when the task successfully completes.
  base::OnceClosure task_success_callback_;

  // Caches the decoding result. Null if decoding is in progress or has failed.
  gfx::ImageSkia image_;

  base::WeakPtrFactory<ImageDecoderTask> weak_ptr_factory_{this};
};

class DeepScanNoticeNotificationDelegate
    : public message_center::NotificationDelegate {
 public:
  explicit DeepScanNoticeNotificationDelegate(base::WeakPtr<Browser> browser)
      : browser_(std::move(browser)) {}

  // message_center::NotificationDelegate
  void Click(const std::optional<int>& button_index,
             const std::optional<std::u16string>& reply) override {
    if (!browser_) {
      return;
    }

    if (browser_->window()->IsMinimized()) {
      browser_->window()->Restore();
    }
    browser_->window()->Activate();

    chrome::ShowSafeBrowsingEnhancedProtection(browser_.get());
  }

 protected:
  ~DeepScanNoticeNotificationDelegate() override = default;

 private:
  base::WeakPtr<Browser> browser_;
};

void ShowDeepScanPromptNotification(Profile* profile) {
  Browser* browser = chrome::FindTabbedBrowser(
      profile,
      /*match_original_profiles=*/false, display::kInvalidDisplayId,
      /*ignore_closing_browsers=*/true);
  message_center::RichNotificationData optional_fields;
  optional_fields.small_image = gfx::Image(gfx::CreateVectorIcon(
      vector_icons::kNotificationDownloadIcon, 20, gfx::kGoogleBlue800));
  message_center::Notification notification(
      message_center::NOTIFICATION_TYPE_SIMPLE, "download_deep_scan_notice",
      /*title=*/u"",
      l10n_util::GetStringUTF16(IDS_DEEP_SCANNING_PROMPT_REMOVAL_NOTIFICATION),
      ui::ImageModel(),
      l10n_util::GetStringUTF16(IDS_DOWNLOAD_NOTIFICATION_DISPLAY_SOURCE),
      GURL(),
      message_center::NotifierId(message_center::NotifierType::APPLICATION,
                                 "download_manager"),
      std::move(optional_fields),
      base::MakeRefCounted<DeepScanNoticeNotificationDelegate>(
          browser->AsWeakPtr()));
  NotificationDisplayService::GetForProfile(profile)->Display(
      NotificationHandler::Type::TRANSIENT, notification, nullptr);
  profile->GetPrefs()->SetBoolean(
      prefs::kSafeBrowsingAutomaticDeepScanningIPHSeen, true);
}

}  // namespace

// DownloadStatusUpdater::Delegate ---------------------------------------------

// The delegate of the `DownloadStatusUpdater` in Lacros Chrome which serves as
// the client for the `DownloadStatusUpdater` in Ash Chrome.
class DownloadStatusUpdater::Delegate
    : public crosapi::mojom::DownloadStatusUpdaterClient {
 public:
  using GetDownloadItemCallback =
      base::RepeatingCallback<download::DownloadItem*(const std::string&)>;

  explicit Delegate(GetDownloadItemCallback get_download_item_callback)
      : get_download_item_callback_(std::move(get_download_item_callback)) {
    CHECK(!get_download_item_callback_.is_null());
    using crosapi::mojom::DownloadStatusUpdater;
    if (auto* remote =
            GetRemote(DownloadStatusUpdater::kBindClientMinVersion)) {
      remote->BindClient(receiver_.BindNewPipeAndPassRemoteWithVersion());
    }
  }

  Delegate(const Delegate&) = delete;
  Delegate& operator=(const Delegate&) = delete;
  ~Delegate() override = default;

  // Updates the remote download if it exists. Returns true on success.
  bool MaybeUpdate(download::DownloadItem* download) {
    auto* const remote = GetRemote();
    if (!remote) {
      return false;
    }

    DownloadItemModel model(
        download, std::make_unique<DownloadUIModel::BubbleStatusTextBuilder>());
    std::vector<DownloadBubbleQuickAction> quick_actions =
        QuickActionsForDownload(model);
    auto status = crosapi::mojom::DownloadStatus::New();
    status->cancellable =
        IsCommandEnabled(quick_actions, DownloadCommands::CANCEL);
    status->full_path = download->GetFullPath();
    status->guid = download->GetGuid();
    status->pausable = IsCommandEnabled(quick_actions, DownloadCommands::PAUSE);
    status->resumable =
        IsCommandEnabled(quick_actions, DownloadCommands::RESUME);
    status->state = download::download_item_utils::ConvertToMojoDownloadState(
        download->GetState());
    status->status_text = model.GetStatusText();
    status->target_file_path = download->GetTargetFilePath();

    const IconAndColor icon_and_color = IconAndColorForDownload(model);
    if (const gfx::VectorIcon* const icon = icon_and_color.icon) {
      status->icons = crosapi::mojom::DownloadStatusIcons::New(
          gfx::CreateVectorIcon(
              *icon, kIconSize,
              GetColor(icon_and_color.color,
                       ui::ColorProviderKey::ColorMode::kDark)),
          gfx::CreateVectorIcon(
              *icon, kIconSize,
              GetColor(icon_and_color.color,
                       ui::ColorProviderKey::ColorMode::kLight)));
    }

    DownloadBubbleProgressBar progress_bar = ProgressBarForDownload(model);
    auto progress = crosapi::mojom::DownloadProgress::New();
    progress->loop = progress_bar.is_looping;
    progress->received_bytes = download->GetReceivedBytes();
    progress->total_bytes = download->GetTotalBytes();
    progress->visible = progress_bar.is_visible;
    status->progress = std::move(progress);

    // If `task` exists and completes, copy the image generated by `task` to
    // `status` and delete `task`; otherwise, posts an image decoder task if
    // conditions satisfied. NOTE: Download updates after image decoding are
    // assumed to be rare.
    const auto* task = static_cast<const ImageDecoderTask*>(
        download->GetUserData(kImageDecoderTaskKey));
    if (task && !task->image().isNull()) {
      status->image = task->image();
      download->RemoveUserData(kImageDecoderTaskKey);
      task = nullptr;
    } else if (!task) {
      MaybePostImageDecoderTask(download);
    }

    remote->Update(std::move(status));
    return true;
  }

 private:
  download::DownloadItem* GetDownloadItem(const std::string& guid) {
    return get_download_item_callback_.Run(guid);
  }

  // crosapi::mojom::DownloadStatusUpdaterClient:
  void Cancel(const std::string& guid, CancelCallback callback) override {
    bool handled = false;
    if (download::DownloadItem* item = GetDownloadItem(guid); item) {
      handled = true;
      item->Cancel(/*user_cancel=*/true);
    }
    std::move(callback).Run(handled);
  }

  void Pause(const std::string& guid, PauseCallback callback) override {
    bool handled = false;
    if (download::DownloadItem* item = GetDownloadItem(guid); item) {
      handled = true;
      if (!item->IsPaused()) {
        item->Pause();
      }
    }
    std::move(callback).Run(handled);
  }

  void Resume(const std::string& guid, ResumeCallback callback) override {
    bool handled = false;
    if (download::DownloadItem* item = GetDownloadItem(guid); item) {
      handled = true;
      if (item->CanResume()) {
        item->Resume(/*user_resume=*/true);
      }
    }
    std::move(callback).Run(handled);
  }

  void ShowInBrowser(const std::string& guid,
                     ShowInBrowserCallback callback) override {
    // Look up the profile from the download item and find a relevant browser to
    // display the download bubble in.
    Profile* profile = nullptr;
    Browser* browser = nullptr;
    if (download::DownloadItem* item = GetDownloadItem(guid); item) {
      content::BrowserContext* browser_context =
          content::DownloadItemUtils::GetBrowserContext(item);
      profile = Profile::FromBrowserContext(browser_context);
      if (profile) {
        // TODO(chlily): This doesn't work for web app initiated downloads.
        browser = chrome::FindTabbedBrowser(profile,
                                            /*match_original_profiles=*/false,
                                            display::kInvalidDisplayId,
                                            /*ignore_closing_browsers=*/true);
      }
    }

    if (browser) {
      // If we found an appropriate browser, show the download bubble in it.
      OnBrowserLocated(guid, std::move(callback), browser);
      return;
    } else if (profile) {
      // Otherwise, attempt to open a new browser window and do the same.
      // This can happen if the last browser window shuts down while there are
      // downloads in progress, and the profile is kept alive. (Some downloads
      // do not block browser shutdown.)
      profiles::OpenBrowserWindowForProfile(
          base::BindOnce(&DownloadStatusUpdater::Delegate::OnBrowserLocated,
                         weak_factory_.GetWeakPtr(), guid, std::move(callback)),
          /*always_create=*/false,
          /*is_new_profile=*/false, /*unblock_extensions=*/true, profile);
      return;
    }
    std::move(callback).Run(/*handled=*/false);
  }

  // Posts an asynchronous task to decode the download image and then updates
  // the download iff:
  // 1. The download file exists and its size is not greater than the threshold.
  // 2. The underlying download is completed.
  // 3. The underlying download is an image download.
  // NOTE: This function should be called only when `download` does not have an
  // associated image decoder task.
  void MaybePostImageDecoderTask(download::DownloadItem* download) {
    CHECK(!download->GetUserData(kImageDecoderTaskKey));

    const base::FilePath& target_file_path = download->GetTargetFilePath();
    if (const std::optional<int64_t>& received_bytes =
            download->GetReceivedBytes();
        target_file_path.empty() || !received_bytes ||
        received_bytes > kImageDecoderTaskMaxFileSize ||
        download->GetState() != download::DownloadItem::COMPLETE ||
        !DownloadItemModel(download).HasSupportedImageMimeType()) {
      return;
    }

    // `download` outlives `image_decoder_task`. Therefore, it is safe to pass
    // `download` to the callback.
    auto image_decoder_task = std::make_unique<ImageDecoderTask>();
    image_decoder_task->Run(
        target_file_path,
        base::BindOnce(
            base::IgnoreResult(&DownloadStatusUpdater::Delegate::MaybeUpdate),
            weak_factory_.GetWeakPtr(), download));
    download->SetUserData(kImageDecoderTaskKey, std::move(image_decoder_task));
  }

  void OnBrowserLocated(const std::string& guid,
                        ShowInBrowserCallback callback,
                        Browser* browser) {
    if (!browser || !browser->window()) {
      std::move(callback).Run(/*handled=*/false);
      return;
    }

    // Activate the browser so that the bubble or chrome://downloads page can be
    // visible.
    if (browser->window()->IsMinimized()) {
      browser->window()->Restore();
    }
    browser->window()->Activate();

    bool showed_bubble = false;
    DownloadBubbleUIController* bubble_controller =
        browser->window()->GetDownloadBubbleUIController();
    // Look up the guid again because the item may have been removed in the
    // meantime.
    if (download::DownloadItem* item = GetDownloadItem(guid);
        item && bubble_controller) {
      offline_items_collection::ContentId content_id =
          OfflineItemUtils::GetContentIdForDownload(item);
      showed_bubble = bubble_controller->OpenMostSpecificDialog(content_id);

      if (item->IsDangerous() && !item->IsDone() && showed_bubble) {
        DownloadItemWarningData::AddWarningActionEvent(
            item,
            DownloadItemWarningData::WarningSurface::DOWNLOAD_NOTIFICATION,
            DownloadItemWarningData::WarningAction::OPEN_SUBPAGE);
      }
    }
    if (!showed_bubble) {
      // Fall back to showing chrome://downloads.
      chrome::ShowDownloads(browser);
    }
    std::move(callback).Run(/*handled=*/true);
  }

  // The receiver bound to `this` for use by crosapi.
  mojo::Receiver<crosapi::mojom::DownloadStatusUpdaterClient> receiver_{this};

  // Callback allowing the lookup of DownloadItem*s from guids.
  GetDownloadItemCallback get_download_item_callback_;

  base::WeakPtrFactory<DownloadStatusUpdater::Delegate> weak_factory_{this};
};

// DownloadStatusUpdater -------------------------------------------------------

DownloadStatusUpdater::DownloadStatusUpdater()
    : delegate_(std::make_unique<Delegate>(
          base::BindRepeating(&DownloadStatusUpdater::GetDownloadItemFromGuid,
                              base::Unretained(this)))) {}

DownloadStatusUpdater::~DownloadStatusUpdater() = default;

void DownloadStatusUpdater::UpdateAppIconDownloadProgress(
    download::DownloadItem* download) {
  if (delegate_->MaybeUpdate(download) && download->IsDangerous()) {
    DownloadItemWarningData::AddWarningActionEvent(
        download,
        DownloadItemWarningData::WarningSurface::DOWNLOAD_NOTIFICATION,
        DownloadItemWarningData::WarningAction::SHOWN);
  }

  Profile* profile = Profile::FromBrowserContext(
      content::DownloadItemUtils::GetBrowserContext(download));
  if (profile &&
      ShouldShowDeepScanPromptNotice(profile, download->GetDangerType())) {
    ShowDeepScanPromptNotification(profile);
  }
}

download::DownloadItem* DownloadStatusUpdater::GetDownloadItemFromGuid(
    const std::string& guid) {
  for (const auto& notifier : notifiers_) {
    content::DownloadManager* manager = notifier->GetManager();
    if (!manager) {
      continue;
    }
    download::DownloadItem* item = manager->GetDownloadByGuid(guid);
    if (item) {
      return item;
    }
  }
  return nullptr;
}