| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // 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 "ash/app_list/app_list_controller_impl.h" |
| #include "ash/app_list/model/app_list_item.h" |
| #include "ash/app_list/model/app_list_model.h" |
| #include "ash/constants/ash_switches.h" |
| #include "ash/public/cpp/accelerators.h" |
| #include "ash/public/cpp/app_list/app_list_types.h" |
| #include "ash/public/cpp/image_downloader.h" |
| #include "ash/public/cpp/shelf_item.h" |
| #include "ash/public/cpp/shelf_types.h" |
| #include "ash/shell.h" |
| #include "base/callback.h" |
| #include "base/run_loop.h" |
| #include "base/scoped_observation.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/apps/app_service/app_icon_factory.h" |
| #include "chrome/browser/apps/app_service/app_service_proxy.h" |
| #include "chrome/browser/apps/app_service/app_service_proxy_factory.h" |
| #include "chrome/browser/ash/login/test/local_policy_test_server_mixin.h" |
| #include "chrome/browser/ash/login/test/session_manager_state_waiter.h" |
| #include "chrome/browser/ash/login/wizard_controller.h" |
| #include "chrome/browser/ash/policy/core/device_policy_cros_browser_test.h" |
| #include "chrome/browser/ash/profiles/profile_helper.h" |
| #include "chrome/browser/ash/remote_apps/id_generator.h" |
| #include "chrome/browser/ash/remote_apps/remote_apps_manager_factory.h" |
| #include "chrome/browser/ash/remote_apps/remote_apps_model.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/app_list/app_list_client_impl.h" |
| #include "chrome/browser/ui/app_list/app_list_syncable_service_factory.h" |
| #include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chromeos/login/auth/user_context.h" |
| #include "components/account_id/account_id.h" |
| #include "components/policy/proto/chrome_device_policy.pb.h" |
| #include "components/services/app_service/public/mojom/types.mojom.h" |
| #include "components/user_manager/user.h" |
| #include "components/user_manager/user_manager.h" |
| #include "components/user_manager/user_type.h" |
| #include "content/public/test/browser_test.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/gfx/image/image_skia.h" |
| #include "ui/gfx/image/image_unittest_util.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| constexpr char kId1[] = "id1"; |
| constexpr char kId2[] = "id2"; |
| constexpr char kId3[] = "id3"; |
| constexpr char kId4[] = "id4"; |
| constexpr char kMissingId[] = "missing_id"; |
| |
| class AppUpdateWaiter : public apps::AppRegistryCache::Observer { |
| public: |
| static base::RepeatingCallback<bool(const apps::AppUpdate&)> IconChanged() { |
| return base::BindRepeating([](const apps::AppUpdate& update) { |
| return !update.StateIsNull() && update.IconKeyChanged(); |
| }); |
| } |
| |
| AppUpdateWaiter( |
| Profile* profile, |
| const std::string& id, |
| base::RepeatingCallback<bool(const apps::AppUpdate&)> condition = |
| base::BindRepeating([](const apps::AppUpdate& update) { |
| return true; |
| })) |
| : id_(id), condition_(condition) { |
| app_registry_cache_ = &apps::AppServiceProxyFactory::GetForProfile(profile) |
| ->AppRegistryCache(); |
| app_registry_cache_observation_.Observe(app_registry_cache_); |
| } |
| |
| void Wait() { |
| if (!condition_met_) { |
| base::RunLoop run_loop; |
| callback_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| // Allow updates to propagate to other observers. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| // apps::AppRegistryCache::Observer: |
| void OnAppUpdate(const apps::AppUpdate& update) override { |
| if (condition_met_ || update.AppId() != id_ || !condition_.Run(update)) |
| return; |
| |
| app_registry_cache_observation_.Reset(); |
| condition_met_ = true; |
| if (callback_) |
| std::move(callback_).Run(); |
| } |
| |
| // apps::AppRegistryCache::Observer: |
| void OnAppRegistryCacheWillBeDestroyed( |
| apps::AppRegistryCache* cache) override { |
| app_registry_cache_observation_.Reset(); |
| } |
| |
| private: |
| std::string id_; |
| apps::AppRegistryCache* app_registry_cache_ = nullptr; |
| base::OnceClosure callback_; |
| base::RepeatingCallback<bool(const apps::AppUpdate&)> condition_; |
| bool condition_met_ = false; |
| base::ScopedObservation<apps::AppRegistryCache, |
| apps::AppRegistryCache::Observer> |
| app_registry_cache_observation_{this}; |
| }; |
| |
| class MockImageDownloader : public RemoteAppsManager::ImageDownloader { |
| public: |
| MOCK_METHOD(void, |
| Download, |
| (const GURL& url, |
| base::OnceCallback<void(const gfx::ImageSkia&)> callback), |
| (override)); |
| }; |
| |
| gfx::ImageSkia CreateTestIcon(int size, SkColor color) { |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(size, size); |
| bitmap.eraseColor(color); |
| return gfx::ImageSkia::CreateFromBitmap(bitmap, 1.0f); |
| } |
| |
| void CheckIconsEqual(const gfx::ImageSkia& expected, |
| const gfx::ImageSkia& actual) { |
| EXPECT_TRUE( |
| gfx::test::AreBitmapsEqual(expected.GetRepresentation(1.0f).GetBitmap(), |
| actual.GetRepresentation(1.0f).GetBitmap())); |
| } |
| |
| } // namespace |
| |
| class RemoteAppsManagerBrowsertest |
| : public policy::DevicePolicyCrosBrowserTest { |
| public: |
| RemoteAppsManagerBrowsertest() : policy::DevicePolicyCrosBrowserTest() {} |
| |
| // DevicePolicyCrosBrowserTest: |
| void SetUp() override { |
| DevicePolicyCrosBrowserTest::SetUp(); |
| app_list::AppListSyncableServiceFactory::SetUseInTesting(true); |
| } |
| |
| // DevicePolicyCrosBrowserTest: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| DevicePolicyCrosBrowserTest::SetUpCommandLine(command_line); |
| command_line->AppendSwitch(switches::kLoginManager); |
| command_line->AppendSwitch(switches::kForceLoginManagerInTests); |
| } |
| |
| // DevicePolicyCrosBrowserTest: |
| void SetUpOnMainThread() override { |
| SetUpDeviceLocalAccountPolicy(); |
| WizardController::SkipPostLoginScreensForTesting(); |
| SessionStateWaiter(session_manager::SessionState::ACTIVE).Wait(); |
| |
| user_manager::User* user = |
| user_manager::UserManager::Get()->GetActiveUser(); |
| profile_ = ProfileHelper::Get()->GetProfileByUser(user); |
| manager_ = RemoteAppsManagerFactory::GetForProfile(profile_); |
| std::unique_ptr<FakeIdGenerator> id_generator = |
| std::make_unique<FakeIdGenerator>( |
| std::vector<std::string>{kId1, kId2, kId3, kId4}); |
| manager_->GetModelForTesting()->SetIdGeneratorForTesting( |
| std::move(id_generator)); |
| std::unique_ptr<MockImageDownloader> image_downloader = |
| std::make_unique<MockImageDownloader>(); |
| image_downloader_ = image_downloader.get(); |
| manager_->SetImageDownloaderForTesting(std::move(image_downloader)); |
| } |
| |
| void SetUpDeviceLocalAccountPolicy() { |
| enterprise_management::DeviceLocalAccountsProto* const |
| device_local_accounts = |
| device_policy()->payload().mutable_device_local_accounts(); |
| enterprise_management::DeviceLocalAccountInfoProto* const account = |
| device_local_accounts->add_account(); |
| account->set_account_id("user@test"); |
| account->set_type(enterprise_management::DeviceLocalAccountInfoProto:: |
| ACCOUNT_TYPE_PUBLIC_SESSION); |
| device_local_accounts->set_auto_login_id("user@test"); |
| device_local_accounts->set_auto_login_delay(0); |
| RefreshDevicePolicy(); |
| } |
| |
| void ExpectImageDownloaderDownload(const GURL& icon_url, |
| const gfx::ImageSkia& icon) { |
| EXPECT_CALL(*image_downloader_, Download(icon_url, testing::_)) |
| .WillOnce( |
| [icon](const GURL& icon_url, |
| base::OnceCallback<void(const gfx::ImageSkia&)> callback) { |
| std::move(callback).Run(icon); |
| }); |
| } |
| |
| ash::AppListItem* GetAppListItem(const std::string& id) { |
| ash::AppListControllerImpl* controller = |
| ash::Shell::Get()->app_list_controller(); |
| ash::AppListModel* model = controller->GetModel(); |
| return model->FindItem(id); |
| } |
| |
| std::string AddApp(const std::string& name, |
| const std::string& folder_id, |
| const GURL& icon_url, |
| bool add_to_front) { |
| base::RunLoop run_loop; |
| std::string id; |
| manager_->AddApp(name, folder_id, icon_url, add_to_front, |
| base::BindOnce( |
| [](base::RepeatingClosure closure, std::string* id_arg, |
| const std::string& id, RemoteAppsError error) { |
| ASSERT_EQ(RemoteAppsError::kNone, error); |
| |
| ash::AppListControllerImpl* controller = |
| ash::Shell::Get()->app_list_controller(); |
| ash::AppListModel* model = controller->GetModel(); |
| ASSERT_TRUE(model->FindItem(id)); |
| *id_arg = id; |
| |
| closure.Run(); |
| }, |
| run_loop.QuitClosure(), &id)); |
| run_loop.Run(); |
| return id; |
| } |
| |
| RemoteAppsError DeleteApp(const std::string& id) { |
| RemoteAppsError error = manager_->DeleteApp(id); |
| // Allow updates to propagate to AppList. |
| base::RunLoop().RunUntilIdle(); |
| return error; |
| } |
| |
| RemoteAppsError DeleteFolder(const std::string& id) { |
| RemoteAppsError error = manager_->DeleteFolder(id); |
| // Allow updates to propagate to AppList. |
| base::RunLoop().RunUntilIdle(); |
| return error; |
| } |
| |
| void AddAppAndWaitForIconChange(const std::string& id, |
| const std::string& name, |
| const std::string& folder_id, |
| const GURL& icon_url, |
| const gfx::ImageSkia& icon, |
| bool add_to_front) { |
| ExpectImageDownloaderDownload(icon_url, icon); |
| AppUpdateWaiter waiter(profile_, id, AppUpdateWaiter::IconChanged()); |
| AddApp(name, folder_id, icon_url, add_to_front); |
| waiter.Wait(); |
| } |
| |
| void AddAppAssertError(RemoteAppsError error, |
| const std::string& name, |
| const std::string& folder_id, |
| const GURL& icon_url, |
| bool add_to_front) { |
| base::RunLoop run_loop; |
| manager_->AddApp( |
| name, folder_id, icon_url, add_to_front, |
| base::BindOnce( |
| [](base::RepeatingClosure closure, RemoteAppsError expected_error, |
| const std::string& id, RemoteAppsError error) { |
| ASSERT_EQ(expected_error, error); |
| closure.Run(); |
| }, |
| run_loop.QuitClosure(), error)); |
| run_loop.Run(); |
| } |
| |
| void ShowLauncherAppsGrid() { |
| AppListClientImpl* client = AppListClientImpl::GetInstance(); |
| EXPECT_FALSE(client->GetAppListWindow()); |
| ash::AcceleratorController::Get()->PerformActionIfEnabled( |
| ash::TOGGLE_APP_LIST_FULLSCREEN, {}); |
| EXPECT_TRUE(client->GetAppListWindow()); |
| } |
| |
| protected: |
| RemoteAppsManager* manager_ = nullptr; |
| MockImageDownloader* image_downloader_ = nullptr; |
| Profile* profile_ = nullptr; |
| LocalPolicyTestServerMixin local_policy_mixin_{&mixin_host_}; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddApp) { |
| // Show launcher UI so that app icons are loaded. |
| ShowLauncherAppsGrid(); |
| |
| std::string name = "name"; |
| GURL icon_url("icon_url"); |
| gfx::ImageSkia icon = CreateTestIcon(32, SK_ColorRED); |
| |
| // App has id kId1. |
| AddAppAndWaitForIconChange(kId1, name, std::string(), icon_url, icon, |
| /*add_to_front=*/false); |
| |
| ash::AppListItem* item = GetAppListItem(kId1); |
| EXPECT_FALSE(item->is_folder()); |
| EXPECT_EQ(name, item->name()); |
| // kShared uses size hint 64 dip. |
| apps::IconEffects icon_effects = |
| base::FeatureList::IsEnabled(features::kAppServiceAdaptiveIcon) |
| ? apps::IconEffects::kCrOsStandardIcon |
| : apps::IconEffects::kResizeAndPad; |
| |
| base::RunLoop run_loop; |
| apps::mojom::IconValuePtr output_data = apps::mojom::IconValue::New(); |
| apps::mojom::IconValuePtr iv = apps::mojom::IconValue::New(); |
| iv->icon_type = apps::mojom::IconType::kStandard; |
| iv->uncompressed = icon; |
| iv->is_placeholder_icon = true; |
| apps::ApplyIconEffects(icon_effects, 64, std::move(iv), |
| base::BindOnce( |
| [](apps::mojom::IconValuePtr* result, |
| base::OnceClosure load_app_icon_callback, |
| apps::mojom::IconValuePtr icon) { |
| *result = std::move(icon); |
| std::move(load_app_icon_callback).Run(); |
| }, |
| &output_data, run_loop.QuitClosure())); |
| run_loop.Run(); |
| CheckIconsEqual(output_data->uncompressed, item->GetDefaultIcon()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddAppError) { |
| std::string name = "name"; |
| GURL icon_url("icon_url"); |
| gfx::ImageSkia icon = CreateTestIcon(32, SK_ColorRED); |
| |
| AddAppAssertError(RemoteAppsError::kFolderIdDoesNotExist, name, kMissingId, |
| icon_url, /*add_to_front=*/false); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddAppErrorNotReady) { |
| std::string name = "name"; |
| GURL icon_url("icon_url"); |
| gfx::ImageSkia icon = CreateTestIcon(32, SK_ColorRED); |
| |
| manager_->SetIsInitializedForTesting(false); |
| AddAppAssertError(RemoteAppsError::kNotReady, name, std::string(), icon_url, |
| /*add_to_front=*/false); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, DeleteApp) { |
| // App has id kId1. |
| AddAppAndWaitForIconChange(kId1, "name", std::string(), GURL("icon_url"), |
| CreateTestIcon(32, SK_ColorRED), |
| /*add_to_front=*/false); |
| |
| RemoteAppsError error = DeleteApp(kId1); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_EQ(RemoteAppsError::kNone, error); |
| EXPECT_FALSE(GetAppListItem(kId1)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, DeleteAppError) { |
| EXPECT_EQ(RemoteAppsError::kAppIdDoesNotExist, DeleteApp(kMissingId)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddAndDeleteFolder) { |
| manager_->AddFolder("folder_name", /*add_to_front=*/false); |
| // Empty folder has no AppListItem. |
| EXPECT_FALSE(GetAppListItem(kId1)); |
| |
| EXPECT_EQ(RemoteAppsError::kNone, DeleteFolder(kId1)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, DeleteFolderError) { |
| EXPECT_EQ(RemoteAppsError::kFolderIdDoesNotExist, DeleteFolder(kMissingId)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddFolderAndApp) { |
| std::string folder_name = "folder_name"; |
| // Folder has id kId1. |
| manager_->AddFolder(folder_name, /*add_to_front=*/false); |
| // Empty folder has no item. |
| EXPECT_FALSE(GetAppListItem(kId1)); |
| |
| // App has id kId2. |
| AddAppAndWaitForIconChange(kId2, "name", kId1, GURL("icon_url"), |
| CreateTestIcon(32, SK_ColorRED), |
| /*add_to_front=*/false); |
| |
| // Folder item was created. |
| ash::AppListItem* folder_item = GetAppListItem(kId1); |
| EXPECT_TRUE(folder_item->is_folder()); |
| EXPECT_EQ(folder_name, folder_item->name()); |
| EXPECT_EQ(1u, folder_item->ChildItemCount()); |
| EXPECT_TRUE(folder_item->FindChildItem(kId2)); |
| |
| ash::AppListItem* item = GetAppListItem(kId2); |
| EXPECT_EQ(kId1, item->folder_id()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, |
| AddFolderWithMultipleApps) { |
| // Folder has id kId1. |
| manager_->AddFolder("folder_name", /*add_to_front=*/false); |
| |
| // App has id kId2. |
| AddAppAndWaitForIconChange(kId2, "name", kId1, GURL("icon_url"), |
| CreateTestIcon(32, SK_ColorRED), |
| /*add_to_front=*/false); |
| // App has id kId3. |
| AddAppAndWaitForIconChange(kId3, "name2", kId1, GURL("icon_url2"), |
| CreateTestIcon(32, SK_ColorBLUE), |
| /*add_to_front=*/false); |
| |
| ash::AppListItem* folder_item = GetAppListItem(kId1); |
| EXPECT_EQ(2u, folder_item->ChildItemCount()); |
| EXPECT_TRUE(folder_item->FindChildItem(kId2)); |
| EXPECT_TRUE(folder_item->FindChildItem(kId3)); |
| |
| DeleteApp(kId2); |
| folder_item = GetAppListItem(kId1); |
| EXPECT_EQ(1u, folder_item->ChildItemCount()); |
| EXPECT_FALSE(folder_item->FindChildItem(kId2)); |
| |
| DeleteApp(kId3); |
| // Empty folder is removed. |
| EXPECT_FALSE(GetAppListItem(kId1)); |
| |
| // App has id kId4. |
| AddAppAndWaitForIconChange(kId4, "name3", kId1, GURL("icon_url3"), |
| CreateTestIcon(32, SK_ColorGREEN), |
| /*add_to_front=*/false); |
| |
| // Folder is re-created. |
| folder_item = GetAppListItem(kId1); |
| EXPECT_EQ(1u, folder_item->ChildItemCount()); |
| EXPECT_TRUE(folder_item->FindChildItem(kId4)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, |
| DeleteFolderWithMultipleApps) { |
| // Folder has id kId1. |
| manager_->AddFolder("folder_name", /*add_to_front=*/false); |
| |
| // App has id kId2. |
| AddAppAndWaitForIconChange(kId2, "name", kId1, GURL("icon_url"), |
| CreateTestIcon(32, SK_ColorRED), |
| /*add_to_front=*/false); |
| // App has id kId3. |
| AddAppAndWaitForIconChange(kId3, "name2", kId1, GURL("icon_url2"), |
| CreateTestIcon(32, SK_ColorBLUE), |
| /*add_to_front=*/false); |
| |
| DeleteFolder(kId1); |
| // Folder is removed. |
| EXPECT_FALSE(GetAppListItem(kId1)); |
| |
| // Apps are moved to top-level. |
| ash::AppListItem* item1 = GetAppListItem(kId2); |
| EXPECT_EQ(std::string(), item1->folder_id()); |
| ash::AppListItem* item2 = GetAppListItem(kId3); |
| EXPECT_EQ(std::string(), item2->folder_id()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddToFront) { |
| // Folder has id kId1. |
| manager_->AddFolder("folder_name", /*add_to_front=*/false); |
| |
| // App has id kId2. |
| AddAppAndWaitForIconChange(kId2, "name", std::string(), GURL("icon_url"), |
| CreateTestIcon(32, SK_ColorRED), |
| /*add_to_front=*/false); |
| |
| EXPECT_FALSE(manager_->ShouldAddToFront(kId1)); |
| EXPECT_FALSE(manager_->ShouldAddToFront(kId2)); |
| |
| // Folder has id kId3. |
| manager_->AddFolder("folder_name2", /*add_to_front=*/true); |
| |
| // App has id kId4. |
| AddAppAndWaitForIconChange(kId4, "name2", kId3, GURL("icon_url"), |
| CreateTestIcon(32, SK_ColorRED), |
| /*add_to_front=*/true); |
| |
| EXPECT_TRUE(manager_->ShouldAddToFront(kId3)); |
| // |add_to_front| disabled since app has a parent folder. |
| EXPECT_FALSE(manager_->ShouldAddToFront(kId4)); |
| } |
| |
| } // namespace ash |