// Copyright 2021 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/ash/projector/projector_client_impl.h"
#include <memory>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/projector/projector_client.h"
#include "ash/public/cpp/projector/projector_new_screencast_precondition.h"
#include "ash/public/cpp/test/mock_projector_client.h"
#include "ash/webui/projector_app/public/cpp/projector_app_constants.h"
#include "ash/webui/system_apps/public/system_web_app_type.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/apps/app_service/app_icon/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/drive/drive_integration_service.h"
#include "chrome/browser/ash/drive/drivefs_test_support.h"
#include "chrome/browser/ash/login/test/device_state_mixin.h"
#include "chrome/browser/ash/login/test/logged_in_user_mixin.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/projector/projector_app_client_impl.h"
#include "chrome/browser/ui/ash/projector/projector_utils.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/web_applications/web_app_command_manager.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/test/base/fake_gaia_mixin.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/mixin_based_in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/account_id/account_id.h"
#include "components/services/app_service/public/cpp/app_registry_cache.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/icon_types.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/page_type.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace ash {
namespace {
apps::AppServiceProxy* GetAppServiceProxy(Profile* profile) {
return apps::AppServiceProxyFactory::GetForProfile(profile);
}
} // namespace
// A class helps to verify enable/disable Drive could invoke
// ProjectorAppClient::Observer::OnDriveFsMountStatusChanged().
class DriveFsMountStatusWaiter : public ProjectorAppClient::Observer {
public:
explicit DriveFsMountStatusWaiter(drive::DriveIntegrationService* service)
: service_(service) {
GetProjectorAppClientImpl()->AddObserver(this);
}
DriveFsMountStatusWaiter(const DriveFsMountStatusWaiter&) = delete;
DriveFsMountStatusWaiter& operator=(const DriveFsMountStatusWaiter&) = delete;
~DriveFsMountStatusWaiter() override {
GetProjectorAppClientImpl()->RemoveObserver(this);
}
// ProjectorAppClient::Observer:
void OnNewScreencastPreconditionChanged(
const NewScreencastPrecondition& condition) override {
std::move(quit_closure_).Run();
}
MOCK_METHOD(void,
OnScreencastsPendingStatusChanged,
(const PendingScreencastContainerSet&),
(override));
MOCK_METHOD(void, OnSodaProgress, (int), (override));
MOCK_METHOD(void, OnSodaError, (), (override));
MOCK_METHOD(void, OnSodaInstalled, (), (override));
void SetDriveEnabled(bool enabled_drive, base::OnceClosure quit_closure) {
quit_closure_ = std::move(quit_closure);
service_->SetEnabled(enabled_drive);
}
ProjectorAppClientImpl* GetProjectorAppClientImpl() {
return static_cast<ProjectorAppClientImpl*>(ProjectorAppClient::Get());
}
private:
base::OnceClosure quit_closure_;
raw_ptr<drive::DriveIntegrationService> service_;
};
class ProjectorClientTest : public InProcessBrowserTest {
public:
ProjectorClientTest() {
scoped_feature_list_.InitAndEnableFeature(
features::kOnDeviceSpeechRecognition);
}
~ProjectorClientTest() override = default;
ProjectorClientTest(const ProjectorClientTest&) = delete;
ProjectorClientTest& operator=(const ProjectorClientTest&) = delete;
// InProcessBrowserTest:
void SetUpInProcessBrowserTestFixture() override {
InProcessBrowserTest::SetUpInProcessBrowserTestFixture();
create_drive_integration_service_ =
base::BindRepeating(&ProjectorClientTest::CreateDriveIntegrationService,
base::Unretained(this));
service_factory_for_test_ = std::make_unique<
drive::DriveIntegrationServiceFactory::ScopedFactoryForTest>(
&create_drive_integration_service_);
}
// This test helper verifies that navigating to the |url| doesn't result in a
// 404 error.
void VerifyUrlValid(const char* url) {
GURL gurl(url);
EXPECT_TRUE(gurl.is_valid()) << "url isn't valid: " << url;
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), gurl))
<< "navigating to url failed: " << url;
content::WebContents* tab =
browser()->tab_strip_model()->GetActiveWebContents();
EXPECT_EQ(tab->GetController().GetLastCommittedEntry()->GetPageType(),
content::PAGE_TYPE_NORMAL)
<< "page has unexpected errors: " << url;
}
drive::DriveIntegrationService* CreateDriveIntegrationService(
Profile* profile) {
base::FilePath mount_path = profile->GetPath().Append("drivefs");
fake_drivefs_helpers_[profile] =
std::make_unique<drive::FakeDriveFsHelper>(profile, mount_path);
// The integration service is owned by `KeyedServiceFactory`.
auto* integration_service = new drive::DriveIntegrationService(
profile, /*test_mount_point_name=*/std::string(), mount_path,
fake_drivefs_helpers_[profile]->CreateFakeDriveFsListenerFactory());
return integration_service;
}
ProjectorClient* client() { return ProjectorClient::Get(); }
private:
drive::DriveIntegrationServiceFactory::FactoryCallback
create_drive_integration_service_;
std::unique_ptr<drive::DriveIntegrationServiceFactory::ScopedFactoryForTest>
service_factory_for_test_;
std::map<Profile*, std::unique_ptr<drive::FakeDriveFsHelper>>
fake_drivefs_helpers_;
base::test::ScopedFeatureList scoped_feature_list_;
};
// This test verifies that the (un)trusted Projector app and annotator WebUI
// URLs are valid.
IN_PROC_BROWSER_TEST_F(ProjectorClientTest, AppUrlsValid) {
VerifyUrlValid(kChromeUIUntrustedProjectorUrl);
}
IN_PROC_BROWSER_TEST_F(ProjectorClientTest, OpenProjectorApp) {
auto* profile = browser()->profile();
SystemWebAppManager::GetForTest(profile)->InstallSystemAppsForTesting();
ui_test_utils::BrowserChangeObserver browser_opened(
nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kAdded);
client()->OpenProjectorApp();
browser_opened.Wait();
// Verify that Projector App is opened.
Browser* app_browser =
FindSystemWebAppBrowser(profile, SystemWebAppType::PROJECTOR);
ASSERT_TRUE(app_browser);
content::WebContents* tab =
app_browser->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(tab);
EXPECT_EQ(tab->GetController().GetVisibleEntry()->GetPageType(),
content::PAGE_TYPE_NORMAL);
}
// This test covers launching the Projector app with files when the app is
// already open. The launch event should recycle the existing window and should
// not open a new window.
IN_PROC_BROWSER_TEST_F(ProjectorClientTest, SendFilesToProjectorApp) {
const size_t starting_browser_count = chrome::GetTotalBrowserCount();
auto* profile = browser()->profile();
SystemWebAppManager::GetForTest(profile)->InstallSystemAppsForTesting();
// Launch the app for the first time.
ui_test_utils::BrowserChangeObserver browser_opened(
nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kAdded);
client()->OpenProjectorApp();
browser_opened.Wait();
// Verify that Projector App is opened.
Browser* app_browser1 =
FindSystemWebAppBrowser(profile, SystemWebAppType::PROJECTOR);
ASSERT_TRUE(app_browser1);
EXPECT_EQ(chrome::GetTotalBrowserCount(), starting_browser_count + 1);
content::WebContents* tab =
app_browser1->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(tab);
EXPECT_TRUE(WaitForLoadStop(tab));
base::FilePath file1("test1"), file2("test2");
// Launch the app again with files. This operation should recycle the same
// window.
SendFilesToProjectorApp({file1, file2});
// Verify that the Projector App is still open.
Browser* app_browser2 =
FindSystemWebAppBrowser(profile, SystemWebAppType::PROJECTOR);
// Launching the app with files should not open a new window.
EXPECT_EQ(app_browser1, app_browser2);
EXPECT_EQ(chrome::GetTotalBrowserCount(), starting_browser_count + 1);
tab = app_browser2->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(tab);
EXPECT_EQ(tab->GetController().GetVisibleEntry()->GetPageType(),
content::PAGE_TYPE_NORMAL);
}
IN_PROC_BROWSER_TEST_F(ProjectorClientTest, MinimizeProjectorApp) {
auto* profile = browser()->profile();
SystemWebAppManager::GetForTest(profile)->InstallSystemAppsForTesting();
ui_test_utils::BrowserChangeObserver browser_opened(
nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kAdded);
client()->OpenProjectorApp();
browser_opened.Wait();
// Verify that Projector App is opened.
Browser* app_browser =
FindSystemWebAppBrowser(profile, SystemWebAppType::PROJECTOR);
ASSERT_TRUE(app_browser);
content::WebContents* tab =
app_browser->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(tab);
EXPECT_EQ(tab->GetController().GetVisibleEntry()->GetPageType(),
content::PAGE_TYPE_NORMAL);
client()->MinimizeProjectorApp();
// Verify that Projector App is minimized.
EXPECT_TRUE(app_browser->window()->IsMinimized());
}
IN_PROC_BROWSER_TEST_F(ProjectorClientTest, CloseProjectorApp) {
auto* profile = browser()->profile();
SystemWebAppManager::GetForTest(profile)->InstallSystemAppsForTesting();
ui_test_utils::BrowserChangeObserver browser_opened(
nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kAdded);
client()->OpenProjectorApp();
browser_opened.Wait();
// Verify that Projector App is opened.
Browser* app_browser =
FindSystemWebAppBrowser(profile, SystemWebAppType::PROJECTOR);
ASSERT_TRUE(app_browser);
content::WebContents* tab =
app_browser->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(tab);
EXPECT_EQ(tab->GetController().GetVisibleEntry()->GetPageType(),
content::PAGE_TYPE_NORMAL);
EXPECT_FALSE(app_browser->IsAttemptingToCloseBrowser());
client()->CloseProjectorApp();
// Verify that Projector App is closing.
EXPECT_TRUE(app_browser->IsAttemptingToCloseBrowser());
}
IN_PROC_BROWSER_TEST_F(ProjectorClientTest, GetDriveFsMountPointPath) {
ASSERT_TRUE(client()->IsDriveFsMounted());
ASSERT_FALSE(client()->IsDriveFsMountFailed());
base::FilePath mounted_path;
ASSERT_TRUE(client()->GetBaseStoragePath(&mounted_path));
ASSERT_EQ(browser()->profile()->GetPath().Append("drivefs"), mounted_path);
}
IN_PROC_BROWSER_TEST_F(ProjectorClientTest, DriveUnmountedAndRemounted) {
drive::DriveIntegrationService* service =
drive::DriveIntegrationServiceFactory::FindForProfile(
browser()->profile());
EXPECT_TRUE(service->is_enabled());
DriveFsMountStatusWaiter observer{service};
{
base::RunLoop run_loop;
observer.SetDriveEnabled(
/*enabled_drive=*/false, run_loop.QuitClosure());
run_loop.Run();
}
{
base::RunLoop run_loop;
observer.SetDriveEnabled(
/*enabled_drive=*/true, run_loop.QuitClosure());
run_loop.Run();
}
}
// Tests Projector client for child and managed users.
class ProjectorClientManagedTest
: public MixinBasedInProcessBrowserTest,
public testing::WithParamInterface</*IsChild=*/bool> {
protected:
void SetUpOnMainThread() override {
MixinBasedInProcessBrowserTest::SetUpOnMainThread();
logged_in_user_mixin_.LogInUser();
}
bool is_child() const { return GetParam(); }
ProjectorClient* client() { return ProjectorClient::Get(); }
std::string GetPolicy() {
if (is_child())
return prefs::kProjectorDogfoodForFamilyLinkEnabled;
return prefs::kProjectorAllowByPolicy;
}
apps::Readiness GetAppReadiness(const webapps::AppId& app_id) {
apps::Readiness readiness;
bool app_found =
GetAppServiceProxy(browser()->profile())
->AppRegistryCache()
.ForOneApp(app_id, [&readiness](const apps::AppUpdate& update) {
readiness = update.Readiness();
});
EXPECT_TRUE(app_found);
return readiness;
}
std::optional<apps::IconKey> GetAppIconKey(const webapps::AppId& app_id) {
std::optional<apps::IconKey> icon_key;
bool app_found =
GetAppServiceProxy(browser()->profile())
->AppRegistryCache()
.ForOneApp(app_id, [&icon_key](const apps::AppUpdate& update) {
icon_key = update.IconKey();
});
EXPECT_TRUE(app_found);
return icon_key;
}
private:
DeviceStateMixin device_state_{
&mixin_host_, DeviceStateMixin::State::OOBE_COMPLETED_CONSUMER_OWNED};
LoggedInUserMixin logged_in_user_mixin_{
&mixin_host_, /*test_base=*/this, embedded_test_server(),
is_child() ? LoggedInUserMixin::LogInType::kChild
: LoggedInUserMixin::LogInType::kManaged};
};
IN_PROC_BROWSER_TEST_P(ProjectorClientManagedTest,
OpenProjectorAppWithoutPolicy) {
auto* profile = browser()->profile();
SystemWebAppManager::GetForTest(profile)->InstallSystemAppsForTesting();
ui_test_utils::BrowserChangeObserver browser_opened(
nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kAdded);
client()->OpenProjectorApp();
if (!is_child())
browser_opened.Wait();
// Verify that Projector App is opened.
Browser* app_browser =
FindSystemWebAppBrowser(profile, ash::SystemWebAppType::PROJECTOR);
if (is_child()) {
// Can't open for Family Link account.
EXPECT_FALSE(app_browser);
} else {
// Can open for other managed account.
EXPECT_TRUE(app_browser);
}
}
IN_PROC_BROWSER_TEST_P(ProjectorClientManagedTest,
PRE_DisableThenEnablePolicy) {
auto* profile = browser()->profile();
// By the time the test runs, SystemWebAppManager already marked the app as
// disabled because the policy is not set. This PRE step, sets the policy so
// that the app is correctly enabled when the actual test runs.
profile->GetPrefs()->SetBoolean(GetPolicy(), true);
}
// Prevents a regression to b/230779397.
IN_PROC_BROWSER_TEST_P(ProjectorClientManagedTest, DisableThenEnablePolicy) {
auto* profile = browser()->profile();
SystemWebAppManager::GetForTest(profile)->InstallSystemAppsForTesting();
ui_test_utils::BrowserChangeObserver browser_opened(
nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kAdded);
client()->OpenProjectorApp();
browser_opened.Wait();
// Verify the user can open the Projector App when the policy is enabled.
Browser* app_browser =
FindSystemWebAppBrowser(profile, SystemWebAppType::PROJECTOR);
ASSERT_TRUE(app_browser);
content::WebContents* tab =
app_browser->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(tab);
EXPECT_EQ(tab->GetController().GetVisibleEntry()->GetPageType(),
content::PAGE_TYPE_NORMAL);
// Suppose the policy flips to false while the user is still signed in and has
// the Projector app open.
profile->GetPrefs()->SetBoolean(GetPolicy(), false);
// The Projector app immediately closes to prevent further access.
EXPECT_TRUE(app_browser->IsAttemptingToCloseBrowser());
auto* web_app_provider = web_app::WebAppProvider::GetForTest(profile);
base::RunLoop loop;
web_app_provider->on_registry_ready().Post(FROM_HERE, loop.QuitClosure());
loop.Run();
web_app_provider->command_manager().AwaitAllCommandsCompleteForTesting();
// We can't uninstall the Projector SWA until the next session, but the icon
// is greyed out and disabled.
EXPECT_EQ(apps::Readiness::kDisabledByPolicy,
GetAppReadiness(ash::kChromeUIUntrustedProjectorSwaAppId));
EXPECT_TRUE(
apps::IconEffects::kBlocked &
GetAppIconKey(ash::kChromeUIUntrustedProjectorSwaAppId)->icon_effects);
// The app can re-enable too if it's already installed and the policy flips to
// true.
profile->GetPrefs()->SetBoolean(GetPolicy(), true);
base::RunLoop loop2;
web_app_provider->on_registry_ready().Post(FROM_HERE, loop2.QuitClosure());
loop2.Run();
web_app_provider->command_manager().AwaitAllCommandsCompleteForTesting();
EXPECT_EQ(apps::Readiness::kReady,
GetAppReadiness(ash::kChromeUIUntrustedProjectorSwaAppId));
EXPECT_FALSE(
apps::IconEffects::kBlocked &
GetAppIconKey(ash::kChromeUIUntrustedProjectorSwaAppId)->icon_effects);
}
INSTANTIATE_TEST_SUITE_P(,
ProjectorClientManagedTest,
/*IsChild=*/testing::Bool());
} // namespace ash