chromium/chrome/browser/ui/webui/ash/app_install/app_install_dialog.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 "chrome/browser/ui/webui/ash/app_install/app_install_dialog.h"

#include <cmath>
#include <utility>
#include <vector>

#include "ash/style/typography.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/apps/almanac_api_client/almanac_app_icon_loader.h"
#include "chrome/browser/apps/app_service/app_icon/app_icon_factory.h"
#include "chrome/browser/apps/app_service/package_id_util.h"
#include "chrome/browser/ui/webui/ash/app_install/app_install.mojom.h"
#include "chrome/browser/ui/webui/ash/app_install/app_install_page_handler.h"
#include "chrome/common/webui_url_constants.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/webapps/common/constants.h"
#include "ui/aura/window.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/base/webui/web_ui_util.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/text_elider.h"
#include "ui/gfx/text_utils.h"

namespace ash::app_install {

namespace {

// Amount of vertical padding from the top of the parent window to show the
// app install dialog. Chosen to overlap the search bar in browser as security
// measure to show that the dialog is not spoofed.
constexpr int kPaddingFromParentTop = 75;

constexpr int kErrorDialogHeight = 228;
constexpr int kMinimumDialogHeight = 282;
constexpr int kDescriptionContainerWidth = 408;
constexpr int kDescriptionLineHeight = 18;
constexpr int kDescriptionVerticalPadding = 24;
constexpr int kScreenshotPadding = 20;
constexpr int kDividerHeight = 1;

int GetDialogHeight(const AppInstallDialogArgs& dialog_args) {
  if (const AppInfoArgs* app_info_args =
          absl::get_if<AppInfoArgs>(&dialog_args)) {
    int height = kMinimumDialogHeight;
    // TODO(b/329515116): Adjust height for long URLs that wrap multiple
    // lines.
    if (app_info_args->data->description.length()) {
      const gfx::FontList font_list =
          TypographyProvider::Get()->ResolveTypographyToken(
              TypographyToken::kCrosAnnotation1);
      float description_width = gfx::GetStringWidth(
          base::UTF8ToUTF16(app_info_args->data->description), font_list);
      int num_lines = std::ceil(description_width / kDescriptionContainerWidth);
      height += (kDescriptionLineHeight * num_lines);
    }
    if (!app_info_args->data->screenshots.empty()) {
      // TODO(b/329515116): This won't work when we show more than one
      // screenshot, if the screenshots are different sizes. The screenshot is
      // displayed at a width of 408px, so calculate the height given that
      // width.
      CHECK(app_info_args->data->screenshots[0]->size.width() != 0);
      height += std::ceil(app_info_args->data->screenshots[0]->size.height() /
                          (app_info_args->data->screenshots[0]->size.width() /
                           float(kDescriptionContainerWidth)));
      height += kScreenshotPadding;
    }
    if (app_info_args->data->description.length() ||
        !app_info_args->data->screenshots.empty()) {
      height += kDividerHeight;
      // The description padding is there even when there is no description.
      height += kDescriptionVerticalPadding;
    }
    return height;
  }

  return kErrorDialogHeight;
}

}  // namespace

// static
base::WeakPtr<AppInstallDialog> AppInstallDialog::CreateDialog() {
  return (new AppInstallDialog())->GetWeakPtr();
}

void AppInstallDialog::ShowApp(
    Profile* profile,
    gfx::NativeWindow parent,
    apps::PackageId package_id,
    std::string app_name,
    GURL app_url,
    std::string app_description,
    std::optional<apps::AppInstallIcon> icon,
    std::vector<mojom::ScreenshotPtr> screenshots,
    base::OnceCallback<void(bool accepted)> dialog_accepted_callback) {
  profile_ = profile->GetWeakPtr();

  if (parent) {
    parent_window_tracker_ = views::NativeWindowTracker::Create(parent);
  }
  parent_ = std::move(parent);

  app_info_args_.package_id = std::move(package_id);
  app_info_args_.data = ash::app_install::mojom::AppInfoData::New();
  app_info_args_.data->url = std::move(app_url);
  app_info_args_.data->name = std::move(app_name);
  app_info_args_.data->description = base::UTF16ToUTF8(gfx::TruncateString(
      base::UTF8ToUTF16(app_description), webapps::kMaximumDescriptionLength,
      gfx::CHARACTER_BREAK));

  // Filter out portrait screenshots.
  app_info_args_.data->screenshots = std::move(screenshots);
  std::erase_if(app_info_args_.data->screenshots,
                [](const mojom::ScreenshotPtr& screenshot) {
                  return screenshot->size.width() < screenshot->size.height() ||
                         screenshot->size.width() == 0;
                });

  app_info_args_.data->is_already_installed =
      apps_util::GetAppWithPackageId(&*profile_, app_info_args_.package_id)
          .has_value();

  app_info_args_.dialog_accepted_callback = std::move(dialog_accepted_callback);

  if (!icon.has_value()) {
    Show(parent, std::move(app_info_args_));
    return;
  }

  icon_loader_ = std::make_unique<apps::AlmanacAppIconLoader>(*profile_.get());
  icon_loader_->GetAppIcon(icon->url, icon->mime_type, icon->is_masking_allowed,
                           base::BindOnce(&AppInstallDialog::OnAppIconLoaded,
                                          weak_factory_.GetWeakPtr()));
}

void AppInstallDialog::ShowNoAppError(gfx::NativeWindow parent) {
  Show(parent, NoAppErrorArgs());
}

void AppInstallDialog::ShowConnectionError(
    gfx::NativeWindow parent,
    base::OnceClosure try_again_callback) {
  ConnectionErrorArgs args;
  args.try_again_callback = std::move(try_again_callback);
  Show(parent, std::move(args));
}

void AppInstallDialog::SetInstallSucceeded() {
  if (dialog_ui_) {
    dialog_ui_->SetInstallComplete(/*success=*/true, std::nullopt);
  }
}

void AppInstallDialog::SetInstallFailed(
    base::OnceCallback<void(bool accepted)> retry_callback) {
  if (dialog_ui_) {
    dialog_ui_->SetInstallComplete(/*success=*/false,
                                   std::move(retry_callback));
  }
}

void AppInstallDialog::CleanUpDialogIfNotShown() {
  if (!dialog_ui_) {
    delete this;
  }
}

void AppInstallDialog::OnDialogShown(content::WebUI* webui) {
  CHECK(dialog_args_.has_value());

  SystemWebDialogDelegate::OnDialogShown(webui);
  dialog_ui_ = static_cast<AppInstallDialogUI*>(webui->GetController());
  dialog_ui_->SetDialogArgs(std::move(dialog_args_).value());
}

bool AppInstallDialog::ShouldShowCloseButton() const {
  return false;
}

void AppInstallDialog::GetDialogSize(gfx::Size* size) const {
  size->SetSize(SystemWebDialogDelegate::kDialogWidth, dialog_height_);
}

AppInstallDialog::AppInstallDialog()
    : SystemWebDialogDelegate(GURL(chrome::kChromeUIAppInstallDialogURL),
                              /*title=*/u"") {}

AppInstallDialog::~AppInstallDialog() = default;

void AppInstallDialog::OnAppIconLoaded(apps::IconValuePtr icon_value) {
  icon_loader_.reset();

  if (icon_value) {
    app_info_args_.data->icon_url =
        GURL(webui::GetBitmapDataUrl(*icon_value->uncompressed.bitmap()));
  }

  gfx::NativeWindow parent =
      (parent_window_tracker_ &&
       parent_window_tracker_->WasNativeWindowDestroyed())
          ? nullptr
          : parent_;
  Show(parent, std::move(app_info_args_));
}

void AppInstallDialog::Show(gfx::NativeWindow parent,
                            AppInstallDialogArgs dialog_args) {
  dialog_args_ = std::move(dialog_args);
  dialog_height_ = GetDialogHeight(dialog_args_.value());

  if (absl::holds_alternative<AppInfoArgs>(dialog_args_.value())) {
    set_dialog_modal_type(ui::mojom::ModalType::kWindow);
  }

  ShowSystemDialog(parent);

  if (!parent) {
    return;
  }

  // Position near the top of the parent window.
  views::Widget* host_widget = views::Widget::GetWidgetForNativeWindow(parent);
  if (!host_widget) {
    return;
  }

  views::Widget* dialog_widget =
      views::Widget::GetWidgetForNativeWindow(dialog_window());

  gfx::Size size = dialog_widget->GetSize();

  int host_width = host_widget->GetWindowBoundsInScreen().width();
  int dialog_width = size.width();
  gfx::Point relative_dialog_position =
      gfx::Point(host_width / 2 - dialog_width / 2, kPaddingFromParentTop);

  gfx::Rect dialog_bounds(relative_dialog_position, size);

  const gfx::Rect absolute_bounds =
      dialog_bounds +
      host_widget->GetClientAreaBoundsInScreen().OffsetFromOrigin();

  dialog_widget->SetBounds(absolute_bounds);
}

base::WeakPtr<AppInstallDialog> AppInstallDialog::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

}  // namespace ash::app_install