chromium/chrome/browser/ash/remote_apps/remote_apps_manager.cc

// Copyright 2020 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/ash/remote_apps/remote_apps_manager.h"

#include <utility>

#include "ash/app_list/app_list_controller_impl.h"
#include "ash/public/cpp/app_menu_constants.h"
#include "ash/public/cpp/image_downloader.h"
#include "ash/shell.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "cc/paint/paint_flags.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/menu_util.h"
#include "chrome/browser/ash/app_list/app_list_model_updater.h"
#include "chrome/browser/ash/app_list/app_list_syncable_service.h"
#include "chrome/browser/ash/app_list/app_list_syncable_service_factory.h"
#include "chrome/browser/ash/app_list/chrome_app_list_item.h"
#include "chrome/browser/ash/app_list/chrome_app_list_model_updater.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/ash/remote_apps/remote_apps_impl.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/app_list/app_list_util.h"
#include "chrome/common/apps/platform_apps/api/enterprise_remote_apps.h"
#include "chrome/grit/generated_resources.h"
#include "components/account_id/account_id.h"
#include "components/services/app_service/public/cpp/menu.h"
#include "components/user_manager/user.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_event_histogram_value.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/canvas_image_source.h"
#include "ui/gfx/image/image_skia.h"

namespace ash {

namespace {

constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("remote_apps_image_downloader", R"(
        semantics {
          sender: "Remote Apps Manager"
          description: "Fetches icons for Remote Apps."
          trigger:
            "Triggered when a Remote App is added to the ChromeOS launcher. "
            "Remote Apps can only be added by allowlisted extensions "
            "installed by enterprise policy."
          data: "No user data."
          destination: OTHER
          destination_other: "Icon URL of the Remote App"
        }
        policy {
          cookies_allowed: NO
          setting: "This request cannot be disabled."
          policy_exception_justification:
            "This request is only performed by allowlisted extensions "
            "installed by enterprise policy."
        }
      )");

class ImageDownloaderImpl : public RemoteAppsManager::ImageDownloader {
 public:
  explicit ImageDownloaderImpl(const Profile* profile) : profile_(profile) {}
  ImageDownloaderImpl(const ImageDownloaderImpl&) = delete;
  ImageDownloaderImpl& operator=(const ImageDownloaderImpl&) = delete;
  ~ImageDownloaderImpl() override = default;

  void Download(const GURL& url, DownloadCallback callback) override {
    ash::ImageDownloader* image_downloader = ash::ImageDownloader::Get();
    DCHECK(image_downloader);
    auto* const user = ProfileHelper::Get()->GetUserByProfile(profile_);
    DCHECK(user);
    const AccountId& account_id = user->GetAccountId();
    image_downloader->Download(url, kTrafficAnnotation, account_id,
                               std::move(callback));
  }

 private:
  const raw_ptr<const Profile> profile_;
};

// Placeholder icon which shows the first letter of the app's name on top of a
// gray circle.
class RemoteAppsPlaceholderIcon : public gfx::CanvasImageSource {
 public:
  RemoteAppsPlaceholderIcon(const std::string& name, int32_t size)
      : gfx::CanvasImageSource(gfx::Size(size, size)) {
    std::u16string sanitized_name = base::UTF8ToUTF16(std::string(name));
    base::i18n::UnadjustStringForLocaleDirection(&sanitized_name);
    letter_ = sanitized_name.substr(0, 1);

    if (size <= 16)
      font_style_ = ui::ResourceBundle::SmallFont;
    else if (size <= 32)
      font_style_ = ui::ResourceBundle::MediumFont;
    else
      font_style_ = ui::ResourceBundle::LargeFont;
  }
  RemoteAppsPlaceholderIcon(const RemoteAppsPlaceholderIcon&) = delete;
  RemoteAppsPlaceholderIcon& operator=(const RemoteAppsPlaceholderIcon&) =
      delete;
  ~RemoteAppsPlaceholderIcon() override = default;

 private:
  // gfx::CanvasImageSource:
  void Draw(gfx::Canvas* canvas) override {
    const gfx::Size& icon_size = size();
    float width = static_cast<float>(icon_size.width());
    float height = static_cast<float>(icon_size.height());

    // Draw gray circle.
    cc::PaintFlags flags;
    flags.setAntiAlias(true);
    flags.setColor(SK_ColorGRAY);
    flags.setStyle(cc::PaintFlags::kFill_Style);
    canvas->DrawCircle(gfx::PointF(width / 2, height / 2), width / 2, flags);

    // Draw the letter on top.
    canvas->DrawStringRectWithFlags(
        letter_,
        ui::ResourceBundle::GetSharedInstance().GetFontList(font_style_),
        SK_ColorWHITE, gfx::Rect(icon_size.width(), icon_size.height()),
        gfx::Canvas::TEXT_ALIGN_CENTER);
  }

  // The first letter of the app's name.
  std::u16string letter_;
  ui::ResourceBundle::FontStyle font_style_ = ui::ResourceBundle::MediumFont;
};

}  // namespace

RemoteAppsManager::RemoteAppsManager(Profile* profile)
    : profile_(profile),
      event_router_(extensions::EventRouter::Get(profile)),
      remote_apps_(std::make_unique<apps::RemoteApps>(
          apps::AppServiceProxyFactory::GetForProfile(profile_),
          this)),
      model_(std::make_unique<RemoteAppsModel>()),
      image_downloader_(std::make_unique<ImageDownloaderImpl>(profile)) {
  remote_apps_->Initialize();
  app_list_syncable_service_ =
      app_list::AppListSyncableServiceFactory::GetForProfile(profile_);
  model_updater_ = app_list_syncable_service_->GetModelUpdater();
  app_list_model_updater_observation_.Observe(model_updater_.get());

  // |AppListSyncableService| manages the Chrome side AppList and has to be
  // initialized before apps can be added.
  if (app_list_syncable_service_->IsInitialized()) {
    Initialize();
  } else {
    app_list_syncable_service_observation_.Observe(
        app_list_syncable_service_.get());
  }
}

RemoteAppsManager::~RemoteAppsManager() = default;

void RemoteAppsManager::Initialize() {
  DCHECK(app_list_syncable_service_->IsInitialized());
  is_initialized_ = true;
}

void RemoteAppsManager::AddApp(const std::string& source_id,
                               const std::string& name,
                               const std::string& folder_id,
                               const GURL& icon_url,
                               bool add_to_front,
                               AddAppCallback callback) {
  if (!is_initialized_) {
    std::move(callback).Run(std::string(), RemoteAppsError::kNotReady);
    return;
  }

  if (!folder_id.empty() && !model_->HasFolder(folder_id)) {
    std::move(callback).Run(std::string(),
                            RemoteAppsError::kFolderIdDoesNotExist);
    return;
  }

  if (!folder_id.empty()) {
    // Disable |add_to_front| if app has a parent folder.
    add_to_front = false;

    // Ensure that the parent folder exists before adding the app.
    MaybeAddFolder(folder_id);
  }

  const RemoteAppsModel::AppInfo& info =
      model_->AddApp(name, icon_url, folder_id, add_to_front);
  add_app_callback_map_.insert({info.id, std::move(callback)});
  remote_apps_->AddApp(info);
  app_id_to_source_id_map_.insert(
      std::pair<std::string, std::string>(info.id, source_id));
}

void RemoteAppsManager::MaybeAddFolder(const std::string& folder_id) {
  // If the specified folder already exists, nothing to do.
  if (model_updater_->FindFolderItem(folder_id))
    return;

  DCHECK(!model_updater_->FindItem(folder_id));

  // The folder to be added.
  auto remote_folder =
      std::make_unique<ChromeAppListItem>(profile_, folder_id, model_updater_);

  const app_list::AppListSyncableService::SyncItem* sync_item =
      app_list_syncable_service_->GetSyncItem(folder_id);
  if (sync_item) {
    // If the specified folder's sync data exists, fill `remote_folder` with
    // the sync data.
    DCHECK_EQ(sync_pb::AppListSpecifics::TYPE_FOLDER, sync_item->item_type);
    remote_folder->SetMetadata(
        app_list::GenerateItemMetadataFromSyncItem(*sync_item));
    remote_folder->SetIsSystemFolder(true);
    remote_folder->SetIsEphemeral(true);
    app_list_syncable_service_->AddItem(std::move(remote_folder));
    return;
  }

  // Handle the case that the specified folder's sync data does not exist.
  DCHECK(model_->HasFolder(folder_id));
  const RemoteAppsModel::FolderInfo& info = model_->GetFolderInfo(folder_id);
  remote_folder->SetChromeName(info.name);
  remote_folder->SetIsSystemFolder(true);
  remote_folder->SetIsEphemeral(true);
  remote_folder->SetChromeIsFolder(true);
  syncer::StringOrdinal position =
      info.add_to_front ? model_updater_->GetPositionBeforeFirstItem()
                        : remote_folder->CalculateDefaultPositionIfApplicable();
  remote_folder->SetChromePosition(position);

  app_list_syncable_service_->AddItem(std::move(remote_folder));
}

const RemoteAppsModel::AppInfo* RemoteAppsManager::GetAppInfo(
    const std::string& app_id) const {
  if (!model_->HasApp(app_id))
    return nullptr;

  return &model_->GetAppInfo(app_id);
}

RemoteAppsError RemoteAppsManager::DeleteApp(const std::string& id) {
  // Check if app was added but |HandleOnAppAdded| has not been called.
  if (!model_->HasApp(id) ||
      add_app_callback_map_.find(id) != add_app_callback_map_.end())
    return RemoteAppsError::kAppIdDoesNotExist;

  model_->DeleteApp(id);
  remote_apps_->DeleteApp(id);
  app_id_to_source_id_map_.erase(id);
  return RemoteAppsError::kNone;
}

void RemoteAppsManager::SortLauncherWithRemoteAppsFirst() {
  static_cast<ChromeAppListModelUpdater*>(model_updater_)
      ->RequestAppListSort(AppListSortOrder::kAlphabeticalEphemeralAppFirst);
}

RemoteAppsError RemoteAppsManager::SetPinnedApps(
    const std::vector<std::string>& app_ids) {
  if (app_ids.size() > 1) {
    return RemoteAppsError::kPinningMultipleAppsNotSupported;
  }

  // Providing an empty app id will reset the pinned app.
  std::string app_id = app_ids.empty() ? "" : app_ids[0];
  bool success =
      Shell::Get()->app_list_controller()->SetHomeButtonQuickApp(app_id);
  return success ? RemoteAppsError::kNone : RemoteAppsError::kFailedToPinAnApp;
}

std::string RemoteAppsManager::AddFolder(const std::string& folder_name,
                                         bool add_to_front) {
  const RemoteAppsModel::FolderInfo& folder_info =
      model_->AddFolder(folder_name, add_to_front);
  return folder_info.id;
}

RemoteAppsError RemoteAppsManager::DeleteFolder(const std::string& folder_id) {
  if (!model_->HasFolder(folder_id))
    return RemoteAppsError::kFolderIdDoesNotExist;

  // Move all items out of the folder. Empty folders are automatically deleted.
  RemoteAppsModel::FolderInfo& folder_info = model_->GetFolderInfo(folder_id);
  for (const auto& app : folder_info.items)
    model_updater_->SetItemFolderId(app, std::string());
  model_->DeleteFolder(folder_id);
  return RemoteAppsError::kNone;
}

bool RemoteAppsManager::ShouldAddToFront(const std::string& id) const {
  if (model_->HasApp(id))
    return model_->GetAppInfo(id).add_to_front;

  if (model_->HasFolder(id))
    return model_->GetFolderInfo(id).add_to_front;

  return false;
}

void RemoteAppsManager::BindFactoryInterface(
    mojo::PendingReceiver<chromeos::remote_apps::mojom::RemoteAppsFactory>
        pending_remote_apps_factory) {
  factory_receivers_.Add(this, std::move(pending_remote_apps_factory));
}

void RemoteAppsManager::BindLacrosBridgeInterface(
    mojo::PendingReceiver<chromeos::remote_apps::mojom::RemoteAppsLacrosBridge>
        pending_remote_apps_lacros_bridge) {
  bridge_receivers_.Add(this, std::move(pending_remote_apps_lacros_bridge));
}

void RemoteAppsManager::Shutdown() {}

void RemoteAppsManager::BindRemoteAppsAndAppLaunchObserver(
    const std::string& source_id,
    mojo::PendingReceiver<chromeos::remote_apps::mojom::RemoteApps>
        pending_remote_apps,
    mojo::PendingRemote<chromeos::remote_apps::mojom::RemoteAppLaunchObserver>
        pending_observer) {
  remote_apps_impl_.BindRemoteAppsAndAppLaunchObserver(
      source_id, std::move(pending_remote_apps), std::move(pending_observer));
}

void RemoteAppsManager::BindRemoteAppsAndAppLaunchObserverForLacros(
    mojo::PendingReceiver<chromeos::remote_apps::mojom::RemoteApps>
        pending_remote_apps,
    mojo::PendingRemote<chromeos::remote_apps::mojom::RemoteAppLaunchObserver>
        pending_observer) {
  remote_apps_impl_.BindRemoteAppsAndAppLaunchObserver(
      std::nullopt, std::move(pending_remote_apps),
      std::move(pending_observer));
}

const std::map<std::string, RemoteAppsModel::AppInfo>&
RemoteAppsManager::GetApps() {
  return model_->GetAllAppInfo();
}

void RemoteAppsManager::LaunchApp(const std::string& app_id) {
  auto it = app_id_to_source_id_map_.find(app_id);
  if (it == app_id_to_source_id_map_.end())
    return;
  std::string source_id = it->second;

  std::unique_ptr<extensions::Event> event = std::make_unique<
      extensions::Event>(
      extensions::events::ENTERPRISE_REMOTE_APPS_ON_REMOTE_APP_LAUNCHED,
      chrome_apps::api::enterprise_remote_apps::OnRemoteAppLaunched::kEventName,
      chrome_apps::api::enterprise_remote_apps::OnRemoteAppLaunched::Create(
          app_id));

  event_router_->DispatchEventToExtension(source_id, std::move(event));

  remote_apps_impl_.OnAppLaunched(source_id, app_id);
}

gfx::ImageSkia RemoteAppsManager::GetIcon(const std::string& id) {
  if (!model_->HasApp(id))
    return gfx::ImageSkia();

  return model_->GetAppInfo(id).icon;
}

gfx::ImageSkia RemoteAppsManager::GetPlaceholderIcon(const std::string& id,
                                                     int32_t size_hint_in_dip) {
  if (!model_->HasApp(id))
    return gfx::ImageSkia();

  gfx::ImageSkia icon(std::make_unique<RemoteAppsPlaceholderIcon>(
                          model_->GetAppInfo(id).name, size_hint_in_dip),
                      gfx::Size(size_hint_in_dip, size_hint_in_dip));
  icon.EnsureRepsForSupportedScales();
  return icon;
}

apps::MenuItems RemoteAppsManager::GetMenuModel(const std::string& id) {
  apps::MenuItems menu_items;
  // TODO(b/236785623): Temporary string for menu item.
  apps::AddCommandItem(ash::LAUNCH_NEW, IDS_APP_CONTEXT_MENU_ACTIVATE_ARC,
                       menu_items);
  return menu_items;
}

void RemoteAppsManager::OnSyncModelUpdated() {
  DCHECK(!is_initialized_);
  Initialize();
  app_list_syncable_service_observation_.Reset();
}

void RemoteAppsManager::OnAppListItemAdded(ChromeAppListItem* item) {
  if (item->is_folder())
    return;

  // Make a copy of id as item->metadata can be invalidated.
  HandleOnAppAdded(std::string(item->id()));
}

void RemoteAppsManager::SetImageDownloaderForTesting(
    std::unique_ptr<ImageDownloader> image_downloader) {
  image_downloader_ = std::move(image_downloader);
}

RemoteAppsModel* RemoteAppsManager::GetModelForTesting() {
  return model_.get();
}

void RemoteAppsManager::SetIsInitializedForTesting(bool is_initialized) {
  is_initialized_ = is_initialized;
}

void RemoteAppsManager::HandleOnAppAdded(const std::string& id) {
  if (!model_->HasApp(id))
    return;
  RemoteAppsModel::AppInfo& app_info = model_->GetAppInfo(id);
  StartIconDownload(id, app_info.icon_url);

  auto it = add_app_callback_map_.find(id);
  DCHECK(it != add_app_callback_map_.end())
      << "Missing callback for id: " << id;
  std::move(it->second).Run(id, RemoteAppsError::kNone);
  add_app_callback_map_.erase(it);
}

void RemoteAppsManager::StartIconDownload(const std::string& id,
                                          const GURL& icon_url) {
  image_downloader_->Download(
      icon_url, base::BindOnce(&RemoteAppsManager::OnIconDownloaded,
                               weak_factory_.GetWeakPtr(), id));
}

void RemoteAppsManager::OnIconDownloaded(const std::string& id,
                                         const gfx::ImageSkia& icon) {
  // App may have been deleted.
  if (!model_->HasApp(id))
    return;

  RemoteAppsModel::AppInfo& app_info = model_->GetAppInfo(id);
  app_info.icon = icon;
  remote_apps_->UpdateAppIcon(id);
}

}  // namespace ash