// Copyright 2016 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/note_taking/note_taking_helper.h"
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include "ash/components/arc/arc_prefs.h"
#include "ash/components/arc/mojom/file_system.mojom.h"
#include "ash/components/arc/mojom/intent_common.mojom.h"
#include "ash/components/arc/mojom/intent_helper.mojom.h"
#include "ash/components/arc/session/arc_bridge_service.h"
#include "ash/components/arc/session/arc_service_manager.h"
#include "ash/components/arc/session/connection_holder.h"
#include "ash/components/arc/test/connection_holder_util.h"
#include "ash/components/arc/test/fake_file_system_instance.h"
#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/note_taking_client.h"
#include "ash/shell.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.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/app_list/arc/arc_app_test.h"
#include "chrome/browser/ash/arc/fileapi/arc_file_system_bridge.h"
#include "chrome/browser/ash/lock_screen_apps/lock_screen_apps.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/ash/note_taking/note_taking_controller_client.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/browser/media/router/media_router_feature.h"
#include "chrome/browser/prefs/browser_prefs.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/web_applications/test/fake_web_app_provider.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/ash/components/dbus/cros_disks/cros_disks_client.h"
#include "chromeos/ash/components/dbus/session_manager/fake_session_manager_client.h"
#include "chromeos/ash/components/dbus/session_manager/session_manager_client.h"
#include "chromeos/ash/components/disks/disk.h"
#include "chromeos/ash/components/disks/disk_mount_manager.h"
#include "components/arc/test/fake_intent_helper_host.h"
#include "components/arc/test/fake_intent_helper_instance.h"
#include "components/crx_file/id_util.h"
#include "components/prefs/pref_service.h"
#include "components/sync_preferences/pref_service_syncable.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/uninstall_reason.h"
#include "extensions/common/api/app_runtime.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_id.h"
#include "mojo/public/cpp/bindings/struct_ptr.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkTypes.h"
#include "ui/display/test/display_manager_test_api.h"
#include "url/gurl.h"
namespace ash {
namespace app_runtime = extensions::api::app_runtime;
using ::arc::mojom::IntentHandlerInfo;
using ::arc::mojom::IntentHandlerInfoPtr;
using ::base::HistogramTester;
using HandledIntent = ::arc::FakeIntentHelperInstance::HandledIntent;
using LaunchResult = NoteTakingHelper::LaunchResult;
namespace {
constexpr LockScreenAppSupport kNotSupported =
LockScreenAppSupport::kNotSupported;
constexpr LockScreenAppSupport kNotAllowedByPolicy =
LockScreenAppSupport::kNotAllowedByPolicy;
constexpr LockScreenAppSupport kSupported = LockScreenAppSupport::kSupported;
constexpr LockScreenAppSupport kEnabled = LockScreenAppSupport::kEnabled;
auto& kDevKeepExtensionId = NoteTakingHelper::kDevKeepExtensionId;
auto& kProdKeepExtensionId = NoteTakingHelper::kProdKeepExtensionId;
// Name of default profile.
const char kTestProfileName[] = "test-profile";
const char kSecondProfileName[] = "second-profile";
// Names for keep apps used in tests.
const char kProdKeepAppName[] = "Google Keep [prod]";
const char kDevKeepAppName[] = "Google Keep [dev]";
// Helper functions returning strings that can be used to compare information
// about available note-taking apps.
std::string ToString(LockScreenAppSupport support) {
std::ostringstream os;
os << support;
return os.str();
}
std::string GetAppString(const std::string& name,
const std::string& id,
bool preferred,
LockScreenAppSupport lock_screen_support) {
return base::StringPrintf("{%s, %s, %d, %s}", name.c_str(), id.c_str(),
preferred, ToString(lock_screen_support).c_str());
}
std::string GetAppString(const NoteTakingAppInfo& info) {
return GetAppString(info.name, info.app_id, info.preferred,
info.lock_screen_support);
}
// Creates an ARC IntentHandlerInfo object.
IntentHandlerInfoPtr CreateIntentHandlerInfo(const std::string& name,
const std::string& package) {
IntentHandlerInfoPtr handler = IntentHandlerInfo::New();
handler->name = name;
handler->package_name = package;
return handler;
}
// Implementation of NoteTakingHelper::Observer for testing.
class TestObserver : public NoteTakingHelper::Observer {
public:
TestObserver() { NoteTakingHelper::Get()->AddObserver(this); }
TestObserver(const TestObserver&) = delete;
TestObserver& operator=(const TestObserver&) = delete;
~TestObserver() override { NoteTakingHelper::Get()->RemoveObserver(this); }
int num_updates() const { return num_updates_; }
void reset_num_updates() { num_updates_ = 0; }
const std::vector<raw_ptr<Profile>> preferred_app_updates() const {
return preferred_app_updates_;
}
void clear_preferred_app_updates() { preferred_app_updates_.clear(); }
private:
// NoteTakingHelper::Observer:
void OnAvailableNoteTakingAppsUpdated() override { num_updates_++; }
void OnPreferredNoteTakingAppUpdated(Profile* profile) override {
preferred_app_updates_.push_back(profile);
}
// Number of times that OnAvailableNoteTakingAppsUpdated() has been called.
int num_updates_ = 0;
// Profiles for which OnPreferredNoteTakingAppUpdated was called.
std::vector<raw_ptr<Profile>> preferred_app_updates_;
};
} // namespace
class NoteTakingHelperTest : public BrowserWithTestWindowTest {
public:
NoteTakingHelperTest() {
// `media_router::kMediaRouter` is disabled because it has unmet
// dependencies and is unrelated to this unit test.
feature_list_.InitAndDisableFeature(media_router::kMediaRouter);
}
NoteTakingHelperTest(const NoteTakingHelperTest&) = delete;
NoteTakingHelperTest& operator=(const NoteTakingHelperTest&) = delete;
~NoteTakingHelperTest() override = default;
void SetUp() override {
ash::ProfileHelper::SetProfileToUserForTestingEnabled(true);
SessionManagerClient::InitializeFakeInMemory();
FakeSessionManagerClient::Get()->set_arc_available(true);
BrowserWithTestWindowTest::SetUp();
InitExtensionService(profile());
InitWebAppProvider(profile());
}
void TearDown() override {
if (initialized_) {
arc::ArcServiceManager::Get()
->arc_bridge_service()
->intent_helper()
->CloseInstance(&intent_helper_);
arc::ArcServiceManager::Get()
->arc_bridge_service()
->file_system()
->CloseInstance(file_system_.get());
NoteTakingHelper::Shutdown();
intent_helper_host_.reset();
file_system_bridge_.reset();
arc_test_.TearDown();
}
extensions::ExtensionSystem::Get(profile())->Shutdown();
BrowserWithTestWindowTest::TearDown();
SessionManagerClient::Shutdown();
ash::ProfileHelper::SetProfileToUserForTestingEnabled(false);
}
protected:
// Information about a Chrome app passed to LaunchChromeApp().
struct ChromeAppLaunchInfo {
extensions::ExtensionId id;
};
// Options that can be passed to Init().
enum InitFlags : uint32_t {
ENABLE_PLAY_STORE = 1 << 0,
ENABLE_PALETTE = 1 << 1,
};
static NoteTakingHelper* helper() { return NoteTakingHelper::Get(); }
LockScreenAppSupport GetLockScreenSupport(Profile* profile,
const std::string& app_id) {
return LockScreenApps::GetSupport(profile, app_id);
}
NoteTakingControllerClient* note_taking_client() {
return helper()->GetNoteTakingControllerClientForTesting();
}
void SetNoteTakingClientProfile(Profile* profile) {
if (note_taking_client())
note_taking_client()->SetProfileForTesting(profile);
}
// Initializes ARC and NoteTakingHelper. |flags| contains OR-ed together
// InitFlags values.
void Init(uint32_t flags) {
ASSERT_FALSE(initialized_);
initialized_ = true;
profile()->GetPrefs()->SetBoolean(arc::prefs::kArcEnabled,
flags & ENABLE_PLAY_STORE);
arc_test_.SetUp(profile());
// Set up FakeIntentHelperHost to emulate full-duplex IntentHelper
// connection.
intent_helper_host_ = std::make_unique<arc::FakeIntentHelperHost>(
arc::ArcServiceManager::Get()->arc_bridge_service()->intent_helper());
arc::ArcServiceManager::Get()
->arc_bridge_service()
->intent_helper()
->SetInstance(&intent_helper_);
WaitForInstanceReady(
arc::ArcServiceManager::Get()->arc_bridge_service()->intent_helper());
file_system_bridge_ = std::make_unique<arc::ArcFileSystemBridge>(
profile(), arc::ArcServiceManager::Get()->arc_bridge_service());
file_system_ = std::make_unique<arc::FakeFileSystemInstance>();
arc::ArcServiceManager::Get()
->arc_bridge_service()
->file_system()
->SetInstance(file_system_.get());
WaitForInstanceReady(
arc::ArcServiceManager::Get()->arc_bridge_service()->file_system());
ASSERT_TRUE(file_system_->InitCalled());
if (flags & ENABLE_PALETTE) {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
switches::kAshForceEnableStylusTools);
}
// TODO(derat): Sigh, something in ArcAppTest appears to be re-enabling ARC.
profile()->GetPrefs()->SetBoolean(arc::prefs::kArcEnabled,
flags & ENABLE_PLAY_STORE);
NoteTakingHelper::Initialize();
NoteTakingHelper::Get()->set_launch_chrome_app_callback_for_test(
base::BindRepeating(&NoteTakingHelperTest::LaunchChromeApp,
base::Unretained(this)));
}
// Creates an extension.
scoped_refptr<const extensions::Extension> CreateExtension(
const extensions::ExtensionId& id,
const std::string& name) {
return CreateExtension(id, name, std::nullopt, std::nullopt);
}
scoped_refptr<const extensions::Extension> CreateExtension(
const extensions::ExtensionId& id,
const std::string& name,
std::optional<base::Value::List> permissions,
std::optional<base::Value::List> action_handlers) {
base::Value::Dict manifest =
base::Value::Dict()
.Set("name", name)
.Set("version", "1.0")
.Set("manifest_version", 2)
.Set("app", base::Value::Dict().Set(
"background",
base::Value::Dict().Set(
"scripts",
base::Value::List().Append("background.js"))));
if (action_handlers)
manifest.Set("action_handlers", std::move(*action_handlers));
if (permissions)
manifest.Set("permissions", std::move(*permissions));
return extensions::ExtensionBuilder()
.SetManifest(std::move(manifest))
.SetID(id)
.Build();
}
void InitWebAppProvider(Profile* profile) {
web_app::test::AwaitStartWebAppProviderAndSubsystems(profile);
}
// Initializes extensions-related objects for |profile|. Tests only need to
// call this if they create additional profiles of their own.
void InitExtensionService(Profile* profile) {
extensions::TestExtensionSystem* extension_system =
static_cast<extensions::TestExtensionSystem*>(
extensions::ExtensionSystem::Get(profile));
extension_system->CreateExtensionService(
base::CommandLine::ForCurrentProcess(),
base::FilePath() /* install_directory */,
false /* autoupdate_enabled */);
}
// Installs or uninstalls |extension| in |profile|.
void InstallExtension(const extensions::Extension* extension,
Profile* profile) {
extensions::ExtensionSystem::Get(profile)
->extension_service()
->AddExtension(extension);
}
void UninstallExtension(const extensions::Extension* extension,
Profile* profile) {
std::u16string error;
extensions::ExtensionSystem::Get(profile)
->extension_service()
->UninstallExtension(
extension->id(),
extensions::UninstallReason::UNINSTALL_REASON_FOR_TESTING, &error);
}
scoped_refptr<const extensions::Extension> CreateAndInstallLockScreenApp(
const std::string& id,
const std::string& app_name,
Profile* profile) {
return CreateAndInstallLockScreenAppWithPermissions(
id, app_name, base::Value::List().Append("lockScreen"), profile);
}
scoped_refptr<const extensions::Extension>
CreateAndInstallLockScreenAppWithPermissions(
const std::string& id,
const std::string& app_name,
std::optional<base::Value::List> permissions,
Profile* profile) {
base::Value::List lock_enabled_action_handler = base::Value::List().Append(
base::Value::Dict()
.Set("action",
app_runtime::ToString(app_runtime::ActionType::kNewNote))
.Set("enabled_on_lock_screen", true));
scoped_refptr<const extensions::Extension> keep_extension =
CreateExtension(id, app_name, std::move(permissions),
std::move(lock_enabled_action_handler));
InstallExtension(keep_extension.get(), profile);
return keep_extension;
}
// BrowserWithTestWindowTest:
std::string GetDefaultProfileName() override { return kTestProfileName; }
// TODO(crbug.com/40286020): merge into BrowserWithTestWindowTest.
void LogIn(const std::string& email) override {
AccountId account_id = AccountId::FromUserEmail(email);
user_manager()->AddUser(account_id);
user_manager()->UserLoggedIn(
account_id,
user_manager::FakeUserManager::GetFakeUsernameHash(account_id),
/*browser_restart=*/false,
/*is_child=*/false);
}
TestingProfile* CreateProfile(const std::string& profile_name) override {
auto prefs =
std::make_unique<sync_preferences::TestingPrefServiceSyncable>();
RegisterUserProfilePrefs(prefs->registry());
profile_prefs_ = prefs.get();
auto* profile = profile_manager()->CreateTestingProfile(
profile_name, std::move(prefs), u"Test profile", 1 /*avatar_id*/,
TestingProfile::TestingFactories());
OnUserProfileCreated(profile_name, profile);
return profile;
}
TestingProfile* CreateAndInitSecondaryProfile() {
auto prefs =
std::make_unique<sync_preferences::TestingPrefServiceSyncable>();
RegisterUserProfilePrefs(prefs->registry());
const AccountId account_id(AccountId::FromUserEmail(kSecondProfileName));
user_manager()->AddUser(account_id);
TestingProfile* profile = profile_manager()->CreateTestingProfile(
kSecondProfileName, std::move(prefs), u"second-profile-username",
/*avatar_id=*/1, TestingProfile::TestingFactories());
OnUserProfileCreated(kSecondProfileName, profile);
InitExtensionService(profile);
InitWebAppProvider(profile);
DCHECK(!ash::ProfileHelper::IsPrimaryProfile(profile));
return profile;
}
std::string NoteAppInfoListToString(
const std::vector<NoteTakingAppInfo>& apps) {
std::vector<std::string> app_strings;
for (const auto& app : apps)
app_strings.push_back(GetAppString(app));
return base::JoinString(app_strings, ",");
}
testing::AssertionResult AvailableAppsMatch(
Profile* profile,
const std::vector<NoteTakingAppInfo>& expected_apps) {
std::vector<NoteTakingAppInfo> actual_apps =
helper()->GetAvailableApps(profile);
if (actual_apps.size() != expected_apps.size()) {
return ::testing::AssertionFailure()
<< "Size mismatch. "
<< "Expected: [" << NoteAppInfoListToString(expected_apps) << "] "
<< "Actual: [" << NoteAppInfoListToString(actual_apps) << "]";
}
std::unique_ptr<::testing::AssertionResult> failure;
for (size_t i = 0; i < expected_apps.size(); ++i) {
std::string expected = GetAppString(expected_apps[i]);
std::string actual = GetAppString(actual_apps[i]);
if (expected != actual) {
if (!failure) {
failure = std::make_unique<::testing::AssertionResult>(
::testing::AssertionFailure());
}
*failure << "Error at index " << i << ": "
<< "Expected: " << expected << " "
<< "Actual: " << actual;
}
}
if (failure)
return *failure;
return ::testing::AssertionSuccess();
}
// Info about launched Chrome apps, in the order they were launched.
std::vector<ChromeAppLaunchInfo> launched_chrome_apps_;
arc::FakeIntentHelperInstance intent_helper_;
std::unique_ptr<arc::ArcFileSystemBridge> file_system_bridge_;
std::unique_ptr<arc::FakeFileSystemInstance> file_system_;
// Pointer to the primary profile (returned by |profile()|) prefs - owned by
// the profile.
raw_ptr<sync_preferences::TestingPrefServiceSyncable, DanglingUntriaged>
profile_prefs_ = nullptr;
private:
// Callback registered with the helper to record Chrome app launch requests.
void LaunchChromeApp(content::BrowserContext* passed_context,
const extensions::Extension* extension,
app_runtime::ActionData action_data) {
EXPECT_EQ(profile(), passed_context);
EXPECT_EQ(app_runtime::ActionType::kNewNote, action_data.action_type);
launched_chrome_apps_.push_back(ChromeAppLaunchInfo{extension->id()});
}
// Has Init() been called?
bool initialized_ = false;
ArcAppTest arc_test_{ArcAppTest::UserManagerMode::kDoNothing};
std::unique_ptr<arc::FakeIntentHelperHost> intent_helper_host_;
base::test::ScopedFeatureList feature_list_;
};
TEST_F(NoteTakingHelperTest, PaletteNotEnabled) {
// Without the palette enabled, IsAppAvailable() should return false.
Init(0);
scoped_refptr<const extensions::Extension> extension =
CreateExtension(kProdKeepExtensionId, "Keep");
InstallExtension(extension.get(), profile());
EXPECT_FALSE(helper()->IsAppAvailable(profile()));
}
TEST_F(NoteTakingHelperTest, ListChromeApps) {
Init(ENABLE_PALETTE);
// Start out without any note-taking apps installed.
EXPECT_FALSE(helper()->IsAppAvailable(profile()));
EXPECT_TRUE(helper()->GetAvailableApps(profile()).empty());
// If only the prod version of the app is installed, it should be returned.
scoped_refptr<const extensions::Extension> prod_extension =
CreateExtension(kProdKeepExtensionId, kProdKeepAppName);
InstallExtension(prod_extension.get(), profile());
EXPECT_TRUE(helper()->IsAppAvailable(profile()));
EXPECT_TRUE(
AvailableAppsMatch(profile(), {{kProdKeepAppName, kProdKeepExtensionId,
false /*preferred*/, kNotSupported}}));
// If the dev version is also installed, it should be listed before the prod
// version.
scoped_refptr<const extensions::Extension> dev_extension =
CreateExtension(kDevKeepExtensionId, kDevKeepAppName);
InstallExtension(dev_extension.get(), profile());
EXPECT_TRUE(
AvailableAppsMatch(profile(), {{kDevKeepAppName, kDevKeepExtensionId,
false /*preferred*/, kNotSupported},
{kProdKeepAppName, kProdKeepExtensionId,
false /*preferred*/, kNotSupported}}));
EXPECT_TRUE(helper()->GetPreferredAppId(profile()).empty());
// Now install a random web app to check that it's ignored.
web_app::test::InstallDummyWebApp(profile(), "Web App",
GURL("http://some.url"));
// Now install a random extension to check that it's ignored.
const extensions::ExtensionId kOtherId = crx_file::id_util::GenerateId("a");
const std::string kOtherName = "Some Other App";
scoped_refptr<const extensions::Extension> other_extension =
CreateExtension(kOtherId, kOtherName);
InstallExtension(other_extension.get(), profile());
EXPECT_TRUE(
AvailableAppsMatch(profile(), {{kDevKeepAppName, kDevKeepExtensionId,
false /*preferred*/, kNotSupported},
{kProdKeepAppName, kProdKeepExtensionId,
false /*preferred*/, kNotSupported}}));
EXPECT_TRUE(helper()->GetPreferredAppId(profile()).empty());
// Mark the prod version as preferred.
helper()->SetPreferredApp(profile(), kProdKeepExtensionId);
EXPECT_TRUE(
AvailableAppsMatch(profile(), {{kDevKeepAppName, kDevKeepExtensionId,
false /*preferred*/, kNotSupported},
{kProdKeepAppName, kProdKeepExtensionId,
true /*preferred*/, kNotSupported}}));
EXPECT_EQ(helper()->GetPreferredAppId(profile()), kProdKeepExtensionId);
EXPECT_EQ(GetLockScreenSupport(profile(), kProdKeepExtensionId),
kNotSupported);
}
TEST_F(NoteTakingHelperTest, ListChromeAppsWithLockScreenNotesSupported) {
Init(ENABLE_PALETTE);
ASSERT_FALSE(helper()->IsAppAvailable(profile()));
ASSERT_TRUE(helper()->GetAvailableApps(profile()).empty());
base::Value::List lock_disabled_action_handler = base::Value::List().Append(
app_runtime::ToString(app_runtime::ActionType::kNewNote));
// Install Keep app that does not support lock screen note taking - it should
// be reported not to support lock screen note taking.
scoped_refptr<const extensions::Extension> prod_extension = CreateExtension(
kProdKeepExtensionId, kProdKeepAppName, /*permissions=*/std::nullopt,
std::move(lock_disabled_action_handler));
InstallExtension(prod_extension.get(), profile());
EXPECT_TRUE(helper()->IsAppAvailable(profile()));
EXPECT_TRUE(
AvailableAppsMatch(profile(), {{kProdKeepAppName, kProdKeepExtensionId,
false /*preferred*/, kNotSupported}}));
EXPECT_TRUE(helper()->GetPreferredAppId(profile()).empty());
// Install additional Keep app - one that supports lock screen note taking.
// This app should be reported to support note taking.
scoped_refptr<const extensions::Extension> dev_extension =
CreateAndInstallLockScreenApp(kDevKeepExtensionId, kDevKeepAppName,
profile());
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, kDevKeepExtensionId, false /*preferred*/, kEnabled},
{kProdKeepAppName, kProdKeepExtensionId, false /*preferred*/,
kNotSupported}}));
EXPECT_TRUE(helper()->GetPreferredAppId(profile()).empty());
}
TEST_F(NoteTakingHelperTest, PreferredAppEnabledOnLockScreen) {
Init(ENABLE_PALETTE);
ASSERT_FALSE(helper()->IsAppAvailable(profile()));
ASSERT_TRUE(helper()->GetAvailableApps(profile()).empty());
// Install lock screen enabled Keep note taking app.
scoped_refptr<const extensions::Extension> dev_extension =
CreateAndInstallLockScreenApp(kDevKeepExtensionId, kDevKeepAppName,
profile());
// Verify that the app is reported to support lock screen note taking.
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, kDevKeepExtensionId, false /*preferred*/, kEnabled}}));
// When the lock screen note taking pref is set and the Keep app is set as the
// preferred note taking app, the app should be reported as selected as lock
// screen note taking app.
helper()->SetPreferredApp(profile(), kDevKeepExtensionId);
helper()->SetPreferredAppEnabledOnLockScreen(profile(), true);
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, kDevKeepExtensionId, true /*preferred*/, kEnabled}}));
EXPECT_EQ(helper()->GetPreferredAppId(profile()), kDevKeepExtensionId);
EXPECT_EQ(GetLockScreenSupport(profile(), kDevKeepExtensionId), kEnabled);
helper()->SetPreferredAppEnabledOnLockScreen(profile(), false);
EXPECT_TRUE(
AvailableAppsMatch(profile(), {{kDevKeepAppName, kDevKeepExtensionId,
true /*preferred*/, kSupported}}));
EXPECT_EQ(helper()->GetPreferredAppId(profile()), kDevKeepExtensionId);
EXPECT_EQ(GetLockScreenSupport(profile(), kDevKeepExtensionId), kSupported);
}
TEST_F(NoteTakingHelperTest, PreferredAppWithNoLockScreenPermission) {
Init(ENABLE_PALETTE);
ASSERT_FALSE(helper()->IsAppAvailable(profile()));
ASSERT_TRUE(helper()->GetAvailableApps(profile()).empty());
// Install lock screen enabled Keep note taking app, but wihtout lock screen
// permission listed.
scoped_refptr<const extensions::Extension> dev_extension =
CreateAndInstallLockScreenAppWithPermissions(
kDevKeepExtensionId, kDevKeepAppName, /*permissions=*/std::nullopt,
profile());
helper()->SetPreferredApp(profile(), kDevKeepExtensionId);
EXPECT_TRUE(
AvailableAppsMatch(profile(), {{kDevKeepAppName, kDevKeepExtensionId,
true /*preferred*/, kNotSupported}}));
EXPECT_EQ(helper()->GetPreferredAppId(profile()), kDevKeepExtensionId);
EXPECT_EQ(GetLockScreenSupport(profile(), kDevKeepExtensionId),
kNotSupported);
}
TEST_F(NoteTakingHelperTest,
PreferredAppWithoutLockSupportClearsLockScreenPref) {
Init(ENABLE_PALETTE);
ASSERT_FALSE(helper()->IsAppAvailable(profile()));
ASSERT_TRUE(helper()->GetAvailableApps(profile()).empty());
// Install dev Keep app that supports lock screen note taking.
scoped_refptr<const extensions::Extension> dev_extension =
CreateAndInstallLockScreenApp(kDevKeepExtensionId, kDevKeepAppName,
profile());
// Install third-party app that doesn't support lock screen note taking.
const extensions::ExtensionId kNewNoteId = crx_file::id_util::GenerateId("a");
const std::string kName = "Some App";
scoped_refptr<const extensions::Extension> has_new_note =
CreateAndInstallLockScreenAppWithPermissions(
kNewNoteId, kName, /*permissions=*/std::nullopt, profile());
// Verify that only Keep app is reported to support lock screen note taking.
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, kDevKeepExtensionId, false /*preferred*/, kEnabled},
{kName, kNewNoteId, false /*preferred*/, kNotSupported}}));
// When the Keep app is set as preferred app, and note taking on lock screen
// is enabled, the keep app should be reported to be selected as the lock
// screen note taking app.
helper()->SetPreferredApp(profile(), kDevKeepExtensionId);
helper()->SetPreferredAppEnabledOnLockScreen(profile(), true);
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, kDevKeepExtensionId, true /*preferred*/, kEnabled},
{kName, kNewNoteId, false /*preferred*/, kNotSupported}}));
// When a third party app (which does not support lock screen note taking) is
// set as the preferred app, Keep app's lock screen support state remain
// enabled - even though it will not be launchable from the lock screen.
helper()->SetPreferredApp(profile(), kNewNoteId);
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, kDevKeepExtensionId, false /*preferred*/, kEnabled},
{kName, kNewNoteId, true /*preferred*/, kNotSupported}}));
EXPECT_EQ(helper()->GetPreferredAppId(profile()), kNewNoteId);
EXPECT_EQ(GetLockScreenSupport(profile(), kNewNoteId), kNotSupported);
}
// Verify the note helper detects apps with "new_note" "action_handler" manifest
// entries.
TEST_F(NoteTakingHelperTest, CustomChromeApps) {
Init(ENABLE_PALETTE);
const extensions::ExtensionId kNewNoteId = crx_file::id_util::GenerateId("a");
const extensions::ExtensionId kEmptyArrayId =
crx_file::id_util::GenerateId("b");
const extensions::ExtensionId kEmptyId = crx_file::id_util::GenerateId("c");
const std::string kName = "Some App";
// "action_handlers": ["new_note"]
scoped_refptr<const extensions::Extension> has_new_note = CreateExtension(
kNewNoteId, kName, /*permissions=*/std::nullopt,
base::Value::List().Append(
app_runtime::ToString(app_runtime::ActionType::kNewNote)));
InstallExtension(has_new_note.get(), profile());
// "action_handlers": []
scoped_refptr<const extensions::Extension> empty_array = CreateExtension(
kEmptyArrayId, kName, /*permissions=*/std::nullopt, base::Value::List());
InstallExtension(empty_array.get(), profile());
// (no action handler entry)
scoped_refptr<const extensions::Extension> none =
CreateExtension(kEmptyId, kName);
InstallExtension(none.get(), profile());
// Only the "new_note" extension is returned from GetAvailableApps.
EXPECT_TRUE(AvailableAppsMatch(
profile(), {{kName, kNewNoteId, false /*preferred*/, kNotSupported}}));
}
// Web apps with a note_taking_new_note_url show as available note-taking apps.
TEST_F(NoteTakingHelperTest, NoteTakingWebAppsListed) {
Init(ENABLE_PALETTE);
{
auto app_info = web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
GURL("http://some1.url"));
app_info->scope = GURL("http://some1.url");
app_info->title = u"Web App 1";
web_app::test::InstallWebApp(profile(), std::move(app_info));
}
std::string app2_id;
{
auto app_info = web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
GURL("http://some2.url"));
app_info->scope = GURL("http://some2.url");
app_info->title = u"Web App 2";
// Set a note_taking_new_note_url on one app.
app_info->note_taking_new_note_url = GURL("http://some2.url/new-note");
app2_id = web_app::test::InstallWebApp(profile(), std::move(app_info));
}
// Check apps were installed.
auto* provider = web_app::WebAppProvider::GetForTest(profile());
EXPECT_EQ(provider->registrar_unsafe().CountUserInstalledApps(), 2);
// Apps with note_taking_new_note_url are listed.
EXPECT_TRUE(AvailableAppsMatch(
profile(), {{"Web App 2", app2_id, false /*preferred*/, kNotSupported}}));
}
// Web apps with a lock_screen_start_url should show as supported on the lock
// screen only when `kWebLockScreenApi` is enabled.
// TODO(crbug.com/40227659): Move this to a lock screen apps unittest file.
TEST_F(NoteTakingHelperTest, LockScreenWebAppsListed) {
Init(ENABLE_PALETTE);
DCHECK(!base::FeatureList::IsEnabled(::features::kWebLockScreenApi));
std::string app1_id;
{
auto app_info = web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
GURL("http://some1.url"));
app_info->scope = GURL("http://some1.url");
app_info->title = u"Web App 1";
// Currently only note-taking apps can be used on the lock screen.
app_info->note_taking_new_note_url = GURL("http://some2.url/new-note");
app1_id = web_app::test::InstallWebApp(profile(), std::move(app_info));
}
std::string app2_id;
{
auto app_info = web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
GURL("http://some2.url"));
app_info->scope = GURL("http://some2.url");
app_info->title = u"Web App 2";
app_info->note_taking_new_note_url = GURL("http://some2.url/new-note");
// Set a lock_screen_start_url on one app.
app_info->lock_screen_start_url =
GURL("http://some2.url/lock-screen-start");
app2_id = web_app::test::InstallWebApp(profile(), std::move(app_info));
}
// Check apps were installed.
auto* provider = web_app::WebAppProvider::GetForTest(profile());
EXPECT_EQ(provider->registrar_unsafe().CountUserInstalledApps(), 2);
// With the flag disabled, web apps are not supported.
EXPECT_TRUE(AvailableAppsMatch(
profile(), {{"Web App 1", app1_id, /*preferred=*/false, kNotSupported},
{"Web App 2", app2_id, /*preferred=*/false, kNotSupported}}));
}
class NoteTakingHelperTest_WebLockScreenApiEnabled
: public NoteTakingHelperTest {
base::test::ScopedFeatureList features_{::features::kWebLockScreenApi};
};
// Web apps with a lock_screen_start_url should show as supported on the lock
// screen only when `kWebLockScreenApi` is enabled.
// TODO(crbug.com/40227659): Move this to a lock screen apps unittest file.
TEST_F(NoteTakingHelperTest_WebLockScreenApiEnabled, LockScreenWebAppsListed) {
Init(ENABLE_PALETTE);
DCHECK(base::FeatureList::IsEnabled(::features::kWebLockScreenApi));
std::string app1_id;
{
auto app_info = web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
GURL("http://some1.url"));
app_info->scope = GURL("http://some1.url");
app_info->title = u"Web App 1";
// Currently only note-taking apps can be used on the lock screen.
app_info->note_taking_new_note_url = GURL("http://some2.url/new-note");
app1_id = web_app::test::InstallWebApp(profile(), std::move(app_info));
}
std::string app2_id;
{
auto app_info = web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
GURL("http://some2.url"));
app_info->scope = GURL("http://some2.url");
app_info->title = u"Web App 2";
app_info->note_taking_new_note_url = GURL("http://some2.url/new-note");
// Set a lock_screen_start_url on one app.
app_info->lock_screen_start_url =
GURL("http://some2.url/lock-screen-start");
app2_id = web_app::test::InstallWebApp(profile(), std::move(app_info));
}
// Check apps were installed.
auto* provider = web_app::WebAppProvider::GetForTest(profile());
EXPECT_EQ(provider->registrar_unsafe().CountUserInstalledApps(), 2);
// The web app with a lock screen start URL is supported.
EXPECT_TRUE(AvailableAppsMatch(
profile(), {{"Web App 1", app1_id, /*preferred=*/false, kNotSupported},
{"Web App 2", app2_id, /*preferred=*/false, kEnabled}}));
}
// Verify that non-allowlisted apps cannot be enabled on lock screen.
TEST_F(NoteTakingHelperTest, CustomLockScreenEnabledApps) {
Init(ENABLE_PALETTE);
const extensions::ExtensionId kNewNoteId = crx_file::id_util::GenerateId("a");
const std::string kName = "Some App";
scoped_refptr<const extensions::Extension> extension =
CreateAndInstallLockScreenApp(kNewNoteId, kName, profile());
EXPECT_TRUE(AvailableAppsMatch(
profile(), {{kName, kNewNoteId, false /*preferred*/, kNotSupported}}));
}
TEST_F(NoteTakingHelperTest, AllowlistedAndCustomAppsShowOnlyOnce) {
Init(ENABLE_PALETTE);
scoped_refptr<const extensions::Extension> extension = CreateExtension(
kProdKeepExtensionId, "Keep", /*permissions=*/std::nullopt,
base::Value::List().Append(
app_runtime::ToString(app_runtime::ActionType::kNewNote)));
InstallExtension(extension.get(), profile());
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{"Keep", kProdKeepExtensionId, false /*preferred*/, kNotSupported}}));
}
TEST_F(NoteTakingHelperTest, LaunchChromeApp) {
Init(ENABLE_PALETTE);
scoped_refptr<const extensions::Extension> extension =
CreateExtension(kProdKeepExtensionId, "Keep");
InstallExtension(extension.get(), profile());
// Check the Chrome app is launched with the correct parameters.
HistogramTester histogram_tester;
helper()->LaunchAppForNewNote(profile());
ASSERT_EQ(1u, launched_chrome_apps_.size());
EXPECT_EQ(kProdKeepExtensionId, launched_chrome_apps_[0].id);
histogram_tester.ExpectUniqueSample(
NoteTakingHelper::kPreferredLaunchResultHistogramName,
static_cast<int>(LaunchResult::NO_APP_SPECIFIED), 1);
histogram_tester.ExpectUniqueSample(
NoteTakingHelper::kDefaultLaunchResultHistogramName,
static_cast<int>(LaunchResult::CHROME_SUCCESS), 1);
}
TEST_F(NoteTakingHelperTest, FallBackIfPreferredAppUnavailable) {
Init(ENABLE_PALETTE);
scoped_refptr<const extensions::Extension> prod_extension =
CreateExtension(kProdKeepExtensionId, "prod");
InstallExtension(prod_extension.get(), profile());
scoped_refptr<const extensions::Extension> dev_extension =
CreateExtension(kDevKeepExtensionId, "dev");
InstallExtension(dev_extension.get(), profile());
{
// Install a default-allowed web app corresponding to ID of
// |NoteTakingHelper::kNoteTakingWebAppIdTest|.
auto app_info = web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
GURL("https://yielding-large-chef.glitch.me/"));
app_info->title = u"Default Allowed Web App";
std::string app_id =
web_app::test::InstallWebApp(profile(), std::move(app_info));
EXPECT_EQ(app_id, NoteTakingHelper::kNoteTakingWebAppIdTest);
}
// Set the prod app as preferred and check that it's launched.
std::unique_ptr<HistogramTester> histogram_tester(new HistogramTester());
helper()->SetPreferredApp(profile(), kProdKeepExtensionId);
helper()->LaunchAppForNewNote(profile());
ASSERT_EQ(1u, launched_chrome_apps_.size());
ASSERT_EQ(kProdKeepExtensionId, launched_chrome_apps_[0].id);
histogram_tester->ExpectUniqueSample(
NoteTakingHelper::kPreferredLaunchResultHistogramName,
static_cast<int>(LaunchResult::CHROME_SUCCESS), 1);
histogram_tester->ExpectTotalCount(
NoteTakingHelper::kDefaultLaunchResultHistogramName, 0);
// Now uninstall the prod app and check that we fall back to the dev app.
UninstallExtension(prod_extension.get(), profile());
launched_chrome_apps_.clear();
histogram_tester = std::make_unique<HistogramTester>();
helper()->LaunchAppForNewNote(profile());
ASSERT_EQ(1u, launched_chrome_apps_.size());
EXPECT_EQ(kDevKeepExtensionId, launched_chrome_apps_[0].id);
histogram_tester->ExpectUniqueSample(
NoteTakingHelper::kPreferredLaunchResultHistogramName,
static_cast<int>(LaunchResult::CHROME_APP_MISSING), 1);
histogram_tester->ExpectUniqueSample(
NoteTakingHelper::kDefaultLaunchResultHistogramName,
static_cast<int>(LaunchResult::CHROME_SUCCESS), 1);
// Now uninstall the dev app and check that we fall back to the test web app.
UninstallExtension(dev_extension.get(), profile());
launched_chrome_apps_.clear();
histogram_tester = std::make_unique<HistogramTester>();
helper()->LaunchAppForNewNote(profile());
// Not a chrome app.
EXPECT_EQ(0u, launched_chrome_apps_.size());
histogram_tester->ExpectUniqueSample(
NoteTakingHelper::kPreferredLaunchResultHistogramName,
static_cast<int>(LaunchResult::CHROME_APP_MISSING), 1);
histogram_tester->ExpectUniqueSample(
NoteTakingHelper::kDefaultLaunchResultHistogramName,
static_cast<int>(LaunchResult::WEB_APP_SUCCESS), 1);
}
TEST_F(NoteTakingHelperTest, PlayStoreInitiallyDisabled) {
Init(ENABLE_PALETTE);
EXPECT_FALSE(helper()->play_store_enabled());
EXPECT_FALSE(helper()->android_apps_received());
// When Play Store is enabled, the helper's members should be updated
// accordingly.
profile()->GetPrefs()->SetBoolean(arc::prefs::kArcEnabled, true);
EXPECT_TRUE(helper()->play_store_enabled());
EXPECT_FALSE(helper()->android_apps_received());
// After the callback to receive intent handlers has run, the "apps received"
// member should be updated (even if there aren't any apps).
helper()->OnIntentFiltersUpdated(std::nullopt);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(helper()->play_store_enabled());
EXPECT_TRUE(helper()->android_apps_received());
}
TEST_F(NoteTakingHelperTest, AddProfileWithPlayStoreEnabled) {
Init(ENABLE_PALETTE);
EXPECT_FALSE(helper()->play_store_enabled());
EXPECT_FALSE(helper()->android_apps_received());
TestObserver observer;
ASSERT_EQ(0, observer.num_updates());
// Add a second profile with the ARC-enabled pref already set. The Play Store
// should be immediately regarded as being enabled and the observer should be
// notified, since OnArcPlayStoreEnabledChanged() apparently isn't called in
// this case: http://crbug.com/700554
auto prefs = std::make_unique<sync_preferences::TestingPrefServiceSyncable>();
RegisterUserProfilePrefs(prefs->registry());
prefs->SetBoolean(arc::prefs::kArcEnabled, true);
profile_manager()->CreateTestingProfile(kSecondProfileName, std::move(prefs),
u"Second User", 1 /* avatar_id */,
TestingProfile::TestingFactories());
EXPECT_TRUE(helper()->play_store_enabled());
EXPECT_FALSE(helper()->android_apps_received());
EXPECT_EQ(1, observer.num_updates());
// TODO(derat|hidehiko): Check that NoteTakingHelper adds itself as an
// observer of the ArcIntentHelperBridge corresponding to the new profile:
// https://crbug.com/748763
// Notification of updated intent filters should result in the apps being
// refreshed.
helper()->OnIntentFiltersUpdated(std::nullopt);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(helper()->play_store_enabled());
EXPECT_TRUE(helper()->android_apps_received());
EXPECT_EQ(2, observer.num_updates());
profile_manager()->DeleteTestingProfile(kSecondProfileName);
}
TEST_F(NoteTakingHelperTest, ListAndroidApps) {
// Add two Android apps.
std::vector<IntentHandlerInfoPtr> handlers;
const std::string kName1 = "App 1";
const std::string kPackage1 = "org.chromium.package1";
handlers.emplace_back(CreateIntentHandlerInfo(kName1, kPackage1));
const std::string kName2 = "App 2";
const std::string kPackage2 = "org.chromium.package2";
handlers.emplace_back(CreateIntentHandlerInfo(kName2, kPackage2));
intent_helper_.SetIntentHandlers(NoteTakingHelper::kIntentAction,
std::move(handlers));
// NoteTakingHelper should make an async request for Android apps when
// constructed.
Init(ENABLE_PALETTE | ENABLE_PLAY_STORE);
EXPECT_TRUE(helper()->play_store_enabled());
EXPECT_FALSE(helper()->android_apps_received());
EXPECT_TRUE(helper()->GetAvailableApps(profile()).empty());
// The apps should be listed after the callback has had a chance to run.
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(helper()->play_store_enabled());
EXPECT_TRUE(helper()->android_apps_received());
EXPECT_TRUE(helper()->IsAppAvailable(profile()));
EXPECT_TRUE(AvailableAppsMatch(
profile(), {{kName1, kPackage1, false /*preferred*/, kNotSupported},
{kName2, kPackage2, false /*preferred*/, kNotSupported}}));
helper()->SetPreferredApp(profile(), kPackage1);
EXPECT_TRUE(AvailableAppsMatch(
profile(), {{kName1, kPackage1, true /*preferred*/, kNotSupported},
{kName2, kPackage2, false /*preferred*/, kNotSupported}}));
// Preferred app is not actually installed, so no app ID should be returned.
EXPECT_TRUE(helper()->GetPreferredAppId(profile()).empty());
EXPECT_EQ(GetLockScreenSupport(profile(), ""), kNotSupported);
// Disable Play Store and check that the apps are no longer returned.
profile()->GetPrefs()->SetBoolean(arc::prefs::kArcEnabled, false);
EXPECT_FALSE(helper()->play_store_enabled());
EXPECT_FALSE(helper()->android_apps_received());
EXPECT_FALSE(helper()->IsAppAvailable(profile()));
EXPECT_TRUE(helper()->GetAvailableApps(profile()).empty());
}
TEST_F(NoteTakingHelperTest, LaunchAndroidAppNoDisplay) {
// Opening Android apps via OpenUrlsWithPermissionAndWindowInfo requires a
// valid internal display, not being able to find one will halt launch.
const std::string kPackage1 = "org.chromium.package1";
std::vector<IntentHandlerInfoPtr> handlers;
handlers.emplace_back(CreateIntentHandlerInfo("App 1", kPackage1));
intent_helper_.SetIntentHandlers(NoteTakingHelper::kIntentAction,
std::move(handlers));
Init(ENABLE_PALETTE | ENABLE_PLAY_STORE);
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(helper()->IsAppAvailable(profile()));
// The installed app fails to launch, registering on histogram.
std::unique_ptr<HistogramTester> histogram_tester(new HistogramTester());
helper()->LaunchAppForNewNote(profile());
ASSERT_EQ(0u, file_system_->handledUrlRequests().size());
histogram_tester->ExpectUniqueSample(
NoteTakingHelper::kPreferredLaunchResultHistogramName,
static_cast<int>(LaunchResult::NO_APP_SPECIFIED), 1);
histogram_tester->ExpectUniqueSample(
NoteTakingHelper::kDefaultLaunchResultHistogramName,
static_cast<int>(LaunchResult::NO_INTERNAL_DISPLAY_FOUND), 1);
}
TEST_F(NoteTakingHelperTest, LaunchAndroidApp) {
// Since now launching Android apps require window info, this step is needed
// to make display info available.
ASSERT_TRUE(Shell::Get());
display::test::DisplayManagerTestApi(Shell::Get()->display_manager())
.SetFirstDisplayAsInternalDisplay();
const std::string kPackage1 = "org.chromium.package1";
std::vector<IntentHandlerInfoPtr> handlers;
handlers.emplace_back(CreateIntentHandlerInfo("App 1", kPackage1));
intent_helper_.SetIntentHandlers(NoteTakingHelper::kIntentAction,
std::move(handlers));
Init(ENABLE_PALETTE | ENABLE_PLAY_STORE);
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(helper()->IsAppAvailable(profile()));
// The installed app should be launched.
std::unique_ptr<HistogramTester> histogram_tester(new HistogramTester());
helper()->LaunchAppForNewNote(profile());
ASSERT_EQ(1u, file_system_->handledUrlRequests().size());
EXPECT_EQ(arc::mojom::ActionType::CREATE_NOTE,
file_system_->handledUrlRequests().at(0)->action_type);
EXPECT_EQ(
kPackage1,
file_system_->handledUrlRequests().at(0)->activity_name->package_name);
EXPECT_EQ(
std::string(),
file_system_->handledUrlRequests().at(0)->activity_name->activity_name);
ASSERT_EQ(0u, file_system_->handledUrlRequests().at(0)->urls.size());
histogram_tester->ExpectUniqueSample(
NoteTakingHelper::kPreferredLaunchResultHistogramName,
static_cast<int>(LaunchResult::NO_APP_SPECIFIED), 1);
histogram_tester->ExpectUniqueSample(
NoteTakingHelper::kDefaultLaunchResultHistogramName,
static_cast<int>(LaunchResult::ANDROID_SUCCESS), 1);
// Install a second app and set it as the preferred app.
const std::string kPackage2 = "org.chromium.package2";
handlers.emplace_back(CreateIntentHandlerInfo("App 1", kPackage1));
handlers.emplace_back(CreateIntentHandlerInfo("App 2", kPackage2));
intent_helper_.SetIntentHandlers(NoteTakingHelper::kIntentAction,
std::move(handlers));
helper()->OnIntentFiltersUpdated(std::nullopt);
base::RunLoop().RunUntilIdle();
helper()->SetPreferredApp(profile(), kPackage2);
// The second app should be launched now.
intent_helper_.clear_handled_intents();
file_system_->clear_handled_requests();
histogram_tester = std::make_unique<HistogramTester>();
helper()->LaunchAppForNewNote(profile());
ASSERT_EQ(1u, file_system_->handledUrlRequests().size());
EXPECT_EQ(arc::mojom::ActionType::CREATE_NOTE,
file_system_->handledUrlRequests().at(0)->action_type);
EXPECT_EQ(
kPackage2,
file_system_->handledUrlRequests().at(0)->activity_name->package_name);
EXPECT_EQ(
std::string(),
file_system_->handledUrlRequests().at(0)->activity_name->activity_name);
ASSERT_EQ(0u, file_system_->handledUrlRequests().at(0)->urls.size());
histogram_tester->ExpectUniqueSample(
NoteTakingHelper::kPreferredLaunchResultHistogramName,
static_cast<int>(LaunchResult::ANDROID_SUCCESS), 1);
histogram_tester->ExpectTotalCount(
NoteTakingHelper::kDefaultLaunchResultHistogramName, 0);
}
TEST_F(NoteTakingHelperTest, NoAppsAvailable) {
Init(ENABLE_PALETTE | ENABLE_PLAY_STORE);
// When no note-taking apps are installed, the histograms should just be
// updated.
HistogramTester histogram_tester;
helper()->LaunchAppForNewNote(profile());
histogram_tester.ExpectUniqueSample(
NoteTakingHelper::kPreferredLaunchResultHistogramName,
static_cast<int>(LaunchResult::NO_APP_SPECIFIED), 1);
histogram_tester.ExpectUniqueSample(
NoteTakingHelper::kDefaultLaunchResultHistogramName,
static_cast<int>(LaunchResult::NO_APPS_AVAILABLE), 1);
}
TEST_F(NoteTakingHelperTest, NotifyObserverAboutAndroidApps) {
Init(ENABLE_PALETTE | ENABLE_PLAY_STORE);
TestObserver observer;
// Let the app-fetching callback run and check that the observer is notified.
base::RunLoop().RunUntilIdle();
EXPECT_EQ(1, observer.num_updates());
// Disabling and enabling Play Store should also notify the observer (and
// enabling should request apps again).
profile()->GetPrefs()->SetBoolean(arc::prefs::kArcEnabled, false);
EXPECT_EQ(2, observer.num_updates());
profile()->GetPrefs()->SetBoolean(arc::prefs::kArcEnabled, true);
EXPECT_EQ(3, observer.num_updates());
// Run ARC data removing operation.
base::RunLoop().RunUntilIdle();
// Update intent filters and check that the observer is notified again after
// apps are received.
helper()->OnIntentFiltersUpdated(std::nullopt);
EXPECT_EQ(3, observer.num_updates());
base::RunLoop().RunUntilIdle();
EXPECT_EQ(4, observer.num_updates());
}
TEST_F(NoteTakingHelperTest, NotifyObserverAboutChromeApps) {
Init(ENABLE_PALETTE);
TestObserver observer;
ASSERT_EQ(0, observer.num_updates());
// Notify that the prod Keep app was installed for the initial profile. Chrome
// extensions are queried dynamically when GetAvailableApps() is called, so we
// don't need to actually install it.
scoped_refptr<const extensions::Extension> keep_extension =
CreateExtension(kProdKeepExtensionId, "Keep");
InstallExtension(keep_extension.get(), profile());
EXPECT_EQ(1, observer.num_updates());
// Unloading the extension should also trigger a notification.
UninstallExtension(keep_extension.get(), profile());
EXPECT_EQ(2, observer.num_updates());
// Non-note-taking apps shouldn't trigger notifications.
scoped_refptr<const extensions::Extension> other_extension =
CreateExtension(crx_file::id_util::GenerateId("a"), "Some Other App");
InstallExtension(other_extension.get(), profile());
EXPECT_EQ(2, observer.num_updates());
UninstallExtension(other_extension.get(), profile());
EXPECT_EQ(2, observer.num_updates());
// Add a second profile and check that it triggers notifications too.
observer.reset_num_updates();
TestingProfile* second_profile = CreateAndInitSecondaryProfile();
DCHECK(ash::ProfileHelper::IsPrimaryProfile(profile()));
DCHECK(!ash::ProfileHelper::IsPrimaryProfile(second_profile));
scoped_refptr<const extensions::Extension> second_keep_extension =
CreateExtension(kProdKeepExtensionId, "Keep");
EXPECT_EQ(0, observer.num_updates());
InstallExtension(second_keep_extension.get(), second_profile);
EXPECT_EQ(1, observer.num_updates());
UninstallExtension(second_keep_extension.get(), second_profile);
EXPECT_EQ(2, observer.num_updates());
profile_manager()->DeleteTestingProfile(kSecondProfileName);
}
TEST_F(NoteTakingHelperTest, NotifyObserverAboutPreferredAppChanges) {
Init(ENABLE_PALETTE);
TestObserver observer;
scoped_refptr<const extensions::Extension> prod_keep_extension =
CreateExtension(kProdKeepExtensionId, "Keep");
InstallExtension(prod_keep_extension.get(), profile());
scoped_refptr<const extensions::Extension> dev_keep_extension =
CreateExtension(kDevKeepExtensionId, "Keep");
InstallExtension(dev_keep_extension.get(), profile());
ASSERT_TRUE(observer.preferred_app_updates().empty());
// Observers should be notified when preferred app is set.
helper()->SetPreferredApp(profile(), prod_keep_extension->id());
EXPECT_EQ(std::vector<raw_ptr<Profile>>{profile()},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// If the preferred app is not changed, observers should not be notified.
helper()->SetPreferredApp(profile(), prod_keep_extension->id());
EXPECT_TRUE(observer.preferred_app_updates().empty());
// Observers should be notified when preferred app is changed.
helper()->SetPreferredApp(profile(), dev_keep_extension->id());
EXPECT_EQ(std::vector<raw_ptr<Profile>>{profile()},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// Observers should be notified when preferred app is cleared.
helper()->SetPreferredApp(profile(), "");
EXPECT_EQ(std::vector<raw_ptr<Profile>>{profile()},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// No change to preferred app.
helper()->SetPreferredApp(profile(), "");
EXPECT_TRUE(observer.preferred_app_updates().empty());
// Initialize secondary profile with a test app.
TestingProfile* second_profile = CreateAndInitSecondaryProfile();
scoped_refptr<const extensions::Extension>
second_profile_prod_keep_extension =
CreateExtension(kProdKeepExtensionId, "Keep");
InstallExtension(second_profile_prod_keep_extension.get(), second_profile);
// Verify that observers are called with the scondary profile if the secondary
// profile preferred app changes.
helper()->SetPreferredApp(second_profile,
second_profile_prod_keep_extension->id());
EXPECT_EQ(std::vector<raw_ptr<Profile>>{second_profile},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// Clearing preferred app in secondary ptofile should fire observers with the
// secondary profile.
helper()->SetPreferredApp(second_profile, "");
EXPECT_EQ(std::vector<raw_ptr<Profile>>{second_profile},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
profile_manager()->DeleteTestingProfile(kSecondProfileName);
}
TEST_F(NoteTakingHelperTest,
NotifyObserverAboutPreferredLockScreenAppSupportChanges) {
Init(ENABLE_PALETTE);
TestObserver observer;
scoped_refptr<const extensions::Extension> dev_extension =
CreateAndInstallLockScreenApp(kDevKeepExtensionId, kDevKeepAppName,
profile());
scoped_refptr<const extensions::Extension> prod_extension =
CreateExtension(kProdKeepExtensionId, "Keep");
InstallExtension(prod_extension.get(), profile());
ASSERT_TRUE(observer.preferred_app_updates().empty());
// Set the app that supports lock screen note taking as preferred.
helper()->SetPreferredApp(profile(), dev_extension->id());
EXPECT_EQ(std::vector<raw_ptr<Profile>>{profile()},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// Disable the preferred app on the lock screen.
EXPECT_TRUE(helper()->SetPreferredAppEnabledOnLockScreen(profile(), false));
EXPECT_EQ(std::vector<raw_ptr<Profile>>{profile()},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// Disabling lock screen support for already enabled app should be no-op.
EXPECT_FALSE(helper()->SetPreferredAppEnabledOnLockScreen(profile(), false));
EXPECT_TRUE(observer.preferred_app_updates().empty());
// Change the state of the preferred app - it should succeed, and a
// notification should be fired.
EXPECT_TRUE(helper()->SetPreferredAppEnabledOnLockScreen(profile(), true));
EXPECT_EQ(std::vector<raw_ptr<Profile>>{profile()},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// No-op, because the preferred app state is not changing.
EXPECT_FALSE(helper()->SetPreferredAppEnabledOnLockScreen(profile(), true));
EXPECT_TRUE(observer.preferred_app_updates().empty());
// Set an app that does not support lock screen as primary.
helper()->SetPreferredApp(profile(), prod_extension->id());
EXPECT_EQ(std::vector<raw_ptr<Profile>>{profile()},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// Chaning state for an app that does not support lock screen note taking
// should be no-op.
EXPECT_FALSE(helper()->SetPreferredAppEnabledOnLockScreen(profile(), false));
EXPECT_TRUE(observer.preferred_app_updates().empty());
}
TEST_F(NoteTakingHelperTest, SetAppEnabledOnLockScreen) {
Init(ENABLE_PALETTE);
TestObserver observer;
scoped_refptr<const extensions::Extension> dev_app =
CreateAndInstallLockScreenApp(kDevKeepExtensionId, kDevKeepAppName,
profile());
scoped_refptr<const extensions::Extension> prod_app =
CreateAndInstallLockScreenApp(kProdKeepExtensionId, kProdKeepAppName,
profile());
const std::string kUnsupportedAppName = "App name";
const extensions::ExtensionId kUnsupportedAppId =
crx_file::id_util::GenerateId("a");
scoped_refptr<const extensions::Extension> unsupported_app =
CreateAndInstallLockScreenAppWithPermissions(
kUnsupportedAppId, kUnsupportedAppName, /*permissions=*/std::nullopt,
profile());
// Disabling preferred app on lock screen should fail if there is no preferred
// app.
EXPECT_FALSE(helper()->SetPreferredAppEnabledOnLockScreen(profile(), true));
helper()->SetPreferredApp(profile(), prod_app->id());
// Setting preferred app should fire observers.
EXPECT_EQ(std::vector<raw_ptr<Profile>>{profile()},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// Verify dev and prod apps are enabled for lock screen, with prod preferred.
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, kDevKeepExtensionId, false /*preferred*/, kEnabled},
{kProdKeepAppName, kProdKeepExtensionId, true /*preferred*/, kEnabled},
{kUnsupportedAppName, kUnsupportedAppId, false /*preferred*/,
kNotSupported}}));
// Allowlist prod app by policy.
profile_prefs_->SetManagedPref(prefs::kNoteTakingAppsLockScreenAllowlist,
base::Value::List().Append(prod_app->id()));
// The preferred app's status hasn't changed, so the observers can remain
// agnostic of the policy change.
EXPECT_TRUE(observer.preferred_app_updates().empty());
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, kDevKeepExtensionId, false /*preferred*/,
kNotAllowedByPolicy},
{kProdKeepAppName, kProdKeepExtensionId, true /*preferred*/, kEnabled},
{kUnsupportedAppName, kUnsupportedAppId, false /*preferred*/,
kNotSupported}}));
// Change allowlist so only dev app is allowlisted.
profile_prefs_->SetManagedPref(prefs::kNoteTakingAppsLockScreenAllowlist,
base::Value::List().Append(dev_app->id()));
// The preferred app status changed, so observers are expected to be notified.
EXPECT_EQ(std::vector<raw_ptr<Profile>>{profile()},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// Preferred app is not enabled on lock screen - chaning the lock screen
// pref should fail.
EXPECT_FALSE(helper()->SetPreferredAppEnabledOnLockScreen(profile(), false));
EXPECT_TRUE(observer.preferred_app_updates().empty());
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, kDevKeepExtensionId, false /*preferred*/, kEnabled},
{kProdKeepAppName, kProdKeepExtensionId, true /*preferred*/,
kNotAllowedByPolicy},
{kUnsupportedAppName, kUnsupportedAppId, false /*preferred*/,
kNotSupported}}));
// Switch preferred note taking app to one that does not support lock screen.
helper()->SetPreferredApp(profile(), unsupported_app->id());
EXPECT_EQ(std::vector<raw_ptr<Profile>>{profile()},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// Policy with an empty allowlist - this should disallow all apps from the
// lock screen.
profile_prefs_->SetManagedPref(prefs::kNoteTakingAppsLockScreenAllowlist,
base::Value::List());
// Preferred app changed notification is not expected if the preferred app is
// not supported on lock screen.
EXPECT_TRUE(observer.preferred_app_updates().empty());
EXPECT_TRUE(
AvailableAppsMatch(profile(), {{kDevKeepAppName, kDevKeepExtensionId,
false /*preferred*/, kNotAllowedByPolicy},
{kProdKeepAppName, kProdKeepExtensionId,
false /*preferred*/, kNotAllowedByPolicy},
{kUnsupportedAppName, kUnsupportedAppId,
true /*preferred*/, kNotSupported}}));
UninstallExtension(dev_app.get(), profile());
UninstallExtension(prod_app.get(), profile());
UninstallExtension(unsupported_app.get(), profile());
profile_prefs_->RemoveManagedPref(prefs::kNoteTakingAppsLockScreenAllowlist);
// No preferred app installed, so no update notification.
EXPECT_TRUE(observer.preferred_app_updates().empty());
}
TEST_F(NoteTakingHelperTest,
UpdateLockScreenSupportStatusWhenAllowlistPolicyRemoved) {
Init(ENABLE_PALETTE);
TestObserver observer;
// Add test app, set it as preferred and enable it on lock screen.
scoped_refptr<const extensions::Extension> app =
CreateAndInstallLockScreenApp(kDevKeepExtensionId, kDevKeepAppName,
profile());
helper()->SetPreferredApp(profile(), app->id());
observer.clear_preferred_app_updates();
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, kDevKeepExtensionId, true /*preferred*/, kEnabled}}));
// Policy with an empty allowlist - this should disallow test app from running
// on lock screen.
profile_prefs_->SetManagedPref(prefs::kNoteTakingAppsLockScreenAllowlist,
base::Value::List());
// Preferred app settings changed - observers should be notified.
EXPECT_EQ(std::vector<raw_ptr<Profile>>{profile()},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// Verify the app is reported as not allowed by policy.
EXPECT_TRUE(AvailableAppsMatch(
profile(), {{kDevKeepAppName, kDevKeepExtensionId, true /*preferred*/,
kNotAllowedByPolicy}}));
// Remove the allowlist policy - the preferred app should become enabled on
// lock screen again.
profile_prefs_->RemoveManagedPref(prefs::kNoteTakingAppsLockScreenAllowlist);
EXPECT_EQ(std::vector<raw_ptr<Profile>>{profile()},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, kDevKeepExtensionId, true /*preferred*/, kEnabled}}));
}
TEST_F(NoteTakingHelperTest,
NoObserverCallsIfPolicyChangesBeforeLockScreenStatusIsFetched) {
Init(ENABLE_PALETTE);
TestObserver observer;
scoped_refptr<const extensions::Extension> app =
CreateAndInstallLockScreenApp(kDevKeepExtensionId, kDevKeepAppName,
profile());
profile_prefs_->SetManagedPref(prefs::kNoteTakingAppsLockScreenAllowlist,
base::Value::List());
// Verify that observers are not notified of preferred app change if preferred
// app is not set when allowlist policy changes.
EXPECT_TRUE(observer.preferred_app_updates().empty());
// Set test app as preferred note taking app.
helper()->SetPreferredApp(profile(), app->id());
EXPECT_EQ(std::vector<raw_ptr<Profile>>{profile()},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// Changing policy before the app's lock screen availability has been reported
// to NoteTakingHelper clients is not expected to fire observers.
profile_prefs_->SetManagedPref(prefs::kNoteTakingAppsLockScreenAllowlist,
base::Value::List().Append(app->id()));
EXPECT_TRUE(observer.preferred_app_updates().empty());
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, kDevKeepExtensionId, true /*preferred*/, kEnabled}}));
}
TEST_F(NoteTakingHelperTest, LockScreenSupportInSecondaryProfile) {
Init(ENABLE_PALETTE);
TestObserver observer;
TestingProfile* second_profile = CreateAndInitSecondaryProfile();
// Add test apps to secondary profile.
scoped_refptr<const extensions::Extension> prod_app =
CreateAndInstallLockScreenApp(kProdKeepExtensionId, kProdKeepAppName,
second_profile);
const std::string kUnsupportedAppName = "App name";
const extensions::ExtensionId kUnsupportedAppId =
crx_file::id_util::GenerateId("a");
scoped_refptr<const extensions::Extension> unsupported_app =
CreateAndInstallLockScreenAppWithPermissions(
kUnsupportedAppId, kUnsupportedAppName, /*permissions=*/std::nullopt,
second_profile);
// Setting preferred app should fire observers for secondary profile.
helper()->SetPreferredApp(second_profile, prod_app->id());
EXPECT_EQ(std::vector<raw_ptr<Profile>>{second_profile},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// Even though prod app supports lock screen it should be reported as not
// supported in the secondary profile.
EXPECT_TRUE(AvailableAppsMatch(second_profile,
{{kProdKeepAppName, kProdKeepExtensionId,
true /*preferred*/, kNotSupported},
{kUnsupportedAppName, kUnsupportedAppId,
false /*preferred*/, kNotSupported}}));
// Enabling an app on lock screen in secondary profile should fail.
EXPECT_FALSE(helper()->SetPreferredAppEnabledOnLockScreen(profile(), true));
auto* profile_prefs =
static_cast<sync_preferences::TestingPrefServiceSyncable*>(
second_profile->GetPrefs());
// Policy with an empty allowlist.
profile_prefs->SetManagedPref(prefs::kNoteTakingAppsLockScreenAllowlist,
base::Value::List());
// Changing policy should not notify observers in secondary profile.
EXPECT_TRUE(observer.preferred_app_updates().empty());
EXPECT_TRUE(AvailableAppsMatch(second_profile,
{{kProdKeepAppName, kProdKeepExtensionId,
true /*preferred*/, kNotSupported},
{kUnsupportedAppName, kUnsupportedAppId,
false /*preferred*/, kNotSupported}}));
EXPECT_EQ(helper()->GetPreferredAppId(second_profile), kProdKeepExtensionId);
EXPECT_EQ(GetLockScreenSupport(second_profile, kProdKeepExtensionId),
kNotSupported);
}
TEST_F(NoteTakingHelperTest, NoteTakingControllerClient) {
Init(ENABLE_PALETTE);
auto has_note_taking_apps = [&]() {
auto* client = NoteTakingClient::GetInstance();
return client && client->CanCreateNote();
};
EXPECT_FALSE(has_note_taking_apps());
{
SetNoteTakingClientProfile(profile());
EXPECT_FALSE(has_note_taking_apps());
scoped_refptr<const extensions::Extension> extension1 =
CreateExtension(kProdKeepExtensionId, kProdKeepAppName);
scoped_refptr<const extensions::Extension> extension2 =
CreateExtension(kDevKeepExtensionId, kDevKeepAppName);
InstallExtension(extension1.get(), profile());
EXPECT_TRUE(has_note_taking_apps());
InstallExtension(extension2.get(), profile());
EXPECT_TRUE(has_note_taking_apps());
UninstallExtension(extension1.get(), profile());
EXPECT_TRUE(has_note_taking_apps());
UninstallExtension(extension2.get(), profile());
EXPECT_FALSE(has_note_taking_apps());
InstallExtension(extension1.get(), profile());
EXPECT_TRUE(has_note_taking_apps());
}
{
TestingProfile* second_profile = CreateAndInitSecondaryProfile();
SetNoteTakingClientProfile(second_profile);
EXPECT_FALSE(has_note_taking_apps());
scoped_refptr<const extensions::Extension> extension1 =
CreateExtension(kProdKeepExtensionId, kProdKeepAppName);
scoped_refptr<const extensions::Extension> extension2 =
CreateExtension(kDevKeepExtensionId, kDevKeepAppName);
InstallExtension(extension2.get(), second_profile);
EXPECT_TRUE(has_note_taking_apps());
SetNoteTakingClientProfile(profile());
EXPECT_TRUE(has_note_taking_apps());
NoteTakingClient::GetInstance()->CreateNote();
ASSERT_EQ(1u, launched_chrome_apps_.size());
ASSERT_EQ(kProdKeepExtensionId, launched_chrome_apps_[0].id);
UninstallExtension(extension2.get(), second_profile);
EXPECT_TRUE(has_note_taking_apps());
profile_manager()->DeleteTestingProfile(kSecondProfileName);
}
}
} // namespace ash