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