// 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