blob: 960a44ecfa7e7e26d796688b457d9bb37cce4df3 [file] [log] [blame]
// Copyright 2016 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/chromeos/note_taking_helper.h"
#include <utility>
#include "ash/public/cpp/ash_switches.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/chromeos/file_manager/path_util.h"
#include "chrome/browser/chromeos/note_taking_controller_client.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/browser/prefs/browser_prefs.h"
#include "chrome/browser/ui/app_list/arc/arc_app_test.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "chromeos/constants/chromeos_switches.h"
#include "chromeos/dbus/session_manager/fake_session_manager_client.h"
#include "components/arc/arc_prefs.h"
#include "components/arc/arc_service_manager.h"
#include "components/arc/arc_util.h"
#include "components/arc/common/intent_helper.mojom.h"
#include "components/arc/intent_helper/arc_intent_helper_bridge.h"
#include "components/arc/session/arc_bridge_service.h"
#include "components/arc/test/connection_holder_util.h"
#include "components/arc/test/fake_intent_helper_instance.h"
#include "components/crx_file/id_util.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_source.h"
#include "extensions/common/api/app_runtime.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/value_builder.h"
#include "url/gurl.h"
namespace app_runtime = extensions::api::app_runtime;
using arc::mojom::IntentHandlerInfo;
using arc::mojom::IntentHandlerInfoPtr;
using base::HistogramTester;
using HandledIntent = arc::FakeIntentHelperInstance::HandledIntent;
namespace chromeos {
using LaunchResult = NoteTakingHelper::LaunchResult;
namespace {
// Name of default profile.
const char kTestProfileName[] = "test-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 GetAppString(const std::string& id,
const std::string& name,
bool preferred,
NoteTakingLockScreenSupport lock_screen_support) {
return base::StringPrintf("{%s, %s, %d, %d}", id.c_str(), name.c_str(),
preferred, static_cast<int>(lock_screen_support));
}
std::string GetAppString(const NoteTakingAppInfo& info) {
return GetAppString(info.app_id, info.name, info.preferred,
info.lock_screen_support);
}
// Helper functions returning strings that can be used to compare launched
// intents.
std::string GetIntentString(const std::string& package,
const std::string& clip_data_uri) {
return base::StringPrintf(
"%s %s", package.c_str(),
clip_data_uri.empty() ? "[unset]" : clip_data_uri.c_str());
}
std::string GetIntentString(const HandledIntent& intent) {
EXPECT_EQ(NoteTakingHelper::kIntentAction, intent.intent->action);
return GetIntentString(
intent.activity->package_name,
(intent.intent->clip_data_uri ? *intent.intent->clip_data_uri : ""));
}
// 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;
}
// Converts a filesystem path to an ARC URL.
std::string GetArcUrl(const base::FilePath& path) {
GURL url;
EXPECT_TRUE(file_manager::util::ConvertPathToArcUrl(path, &url));
return url.spec();
}
// Implementation of NoteTakingHelper::Observer for testing.
class TestObserver : public NoteTakingHelper::Observer {
public:
TestObserver() { NoteTakingHelper::Get()->AddObserver(this); }
~TestObserver() override { NoteTakingHelper::Get()->RemoveObserver(this); }
int num_updates() const { return num_updates_; }
void reset_num_updates() { num_updates_ = 0; }
const std::vector<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<Profile*> preferred_app_updates_;
DISALLOW_COPY_AND_ASSIGN(TestObserver);
};
} // namespace
class NoteTakingHelperTest : public BrowserWithTestWindowTest {
public:
NoteTakingHelperTest() = default;
~NoteTakingHelperTest() override = default;
void SetUp() override {
chromeos::SessionManagerClient::InitializeFakeInMemory();
chromeos::FakeSessionManagerClient::Get()->set_arc_available(true);
BrowserWithTestWindowTest::SetUp();
InitExtensionService(profile());
}
void TearDown() override {
if (initialized_) {
arc::ArcServiceManager::Get()
->arc_bridge_service()
->intent_helper()
->CloseInstance(&intent_helper_);
NoteTakingHelper::Shutdown();
intent_helper_bridge_.reset();
arc_test_.TearDown();
}
extensions::ExtensionSystem::Get(profile())->Shutdown();
BrowserWithTestWindowTest::TearDown();
chromeos::SessionManagerClient::Shutdown();
}
protected:
// Information about a Chrome app passed to LaunchChromeApp().
struct ChromeAppLaunchInfo {
extensions::ExtensionId id;
base::FilePath path;
};
// 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(); }
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 ArcIntentHelperBridge to emulate full-duplex IntentHelper
// connection.
intent_helper_bridge_ = std::make_unique<arc::ArcIntentHelperBridge>(
profile(), arc::ArcServiceManager::Get()->arc_bridge_service());
arc::ArcServiceManager::Get()
->arc_bridge_service()
->intent_helper()
->SetInstance(&intent_helper_);
WaitForInstanceReady(
arc::ArcServiceManager::Get()->arc_bridge_service()->intent_helper());
if (flags & ENABLE_PALETTE) {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
ash::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()->SetProfileWithEnabledLockScreenApps(profile());
NoteTakingHelper::Get()->set_launch_chrome_app_callback_for_test(base::Bind(
&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, nullptr, nullptr);
}
scoped_refptr<const extensions::Extension> CreateExtension(
const extensions::ExtensionId& id,
const std::string& name,
std::unique_ptr<base::Value> permissions,
std::unique_ptr<base::Value> action_handlers) {
std::unique_ptr<base::DictionaryValue> manifest =
extensions::DictionaryBuilder()
.Set("name", name)
.Set("version", "1.0")
.Set("manifest_version", 2)
.Set("app",
extensions::DictionaryBuilder()
.Set("background",
extensions::DictionaryBuilder()
.Set("scripts", extensions::ListBuilder()
.Append("background.js")
.Build())
.Build())
.Build())
.Build();
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();
}
// 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) {
base::string16 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, extensions::ListBuilder().Append("lockScreen").Build(),
profile);
}
scoped_refptr<const extensions::Extension>
CreateAndInstallLockScreenAppWithPermissions(
const std::string& id,
const std::string& app_name,
std::unique_ptr<base::ListValue> permissions,
Profile* profile) {
std::unique_ptr<base::Value> lock_enabled_action_handler =
extensions::ListBuilder()
.Append(extensions::DictionaryBuilder()
.Set("action", app_runtime::ToString(
app_runtime::ACTION_TYPE_NEW_NOTE))
.Set("enabled_on_lock_screen", true)
.Build())
.Build();
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:
TestingProfile* CreateProfile() override {
auto prefs =
std::make_unique<sync_preferences::TestingPrefServiceSyncable>();
RegisterUserProfilePrefs(prefs->registry());
profile_prefs_ = prefs.get();
return profile_manager()->CreateTestingProfile(
kTestProfileName, std::move(prefs), base::ASCIIToUTF16("Test profile"),
1 /*avatar_id*/, std::string() /*supervised_user_id*/,
TestingProfile::TestingFactories());
}
testing::AssertionResult PreferredAppMatches(Profile* profile,
NoteTakingAppInfo app_info) {
std::unique_ptr<NoteTakingAppInfo> preferred_app =
helper()->GetPreferredChromeAppInfo(profile);
if (!preferred_app)
return ::testing::AssertionFailure() << "No preferred app";
std::string expected = GetAppString(app_info);
std::string actual = GetAppString(*preferred_app);
if (expected != actual) {
return ::testing::AssertionFailure() << "Expected: " << expected << " "
<< "Actual: " << actual;
}
return ::testing::AssertionSuccess();
}
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_;
// Pointer to the primary profile (returned by |profile()|) prefs - owned by
// the profile.
sync_preferences::TestingPrefServiceSyncable* 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,
std::unique_ptr<app_runtime::ActionData> action_data,
const base::FilePath& path) {
EXPECT_EQ(profile(), passed_context);
EXPECT_EQ(app_runtime::ActionType::ACTION_TYPE_NEW_NOTE,
action_data->action_type);
launched_chrome_apps_.push_back(ChromeAppLaunchInfo{extension->id(), path});
}
// Has Init() been called?
bool initialized_ = false;
ArcAppTest arc_test_;
std::unique_ptr<arc::ArcIntentHelperBridge> intent_helper_bridge_;
DISALLOW_COPY_AND_ASSIGN(NoteTakingHelperTest);
};
TEST_F(NoteTakingHelperTest, PaletteNotEnabled) {
// Without the palette enabled, IsAppAvailable() should return false.
Init(0);
scoped_refptr<const extensions::Extension> extension =
CreateExtension(NoteTakingHelper::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(NoteTakingHelper::kProdKeepExtensionId, kProdKeepAppName);
InstallExtension(prod_extension.get(), profile());
EXPECT_TRUE(helper()->IsAppAvailable(profile()));
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kProdKeepAppName, NoteTakingHelper::kProdKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kNotSupported}}));
// If the dev version is also installed, it should be listed before the prod
// version.
scoped_refptr<const extensions::Extension> dev_extension =
CreateExtension(NoteTakingHelper::kDevKeepExtensionId, kDevKeepAppName);
InstallExtension(dev_extension.get(), profile());
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kNotSupported},
{kProdKeepAppName, NoteTakingHelper::kProdKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kNotSupported}}));
EXPECT_TRUE(helper()->GetPreferredChromeAppInfo(profile()));
// Now install a random extension and 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, NoteTakingHelper::kDevKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kNotSupported},
{kProdKeepAppName, NoteTakingHelper::kProdKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kNotSupported}}));
EXPECT_TRUE(helper()->GetPreferredChromeAppInfo(profile()));
// Mark the prod version as preferred.
helper()->SetPreferredApp(profile(), NoteTakingHelper::kProdKeepExtensionId);
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kNotSupported},
{kProdKeepAppName, NoteTakingHelper::kProdKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kNotSupported}}));
EXPECT_TRUE(PreferredAppMatches(
profile(),
{kProdKeepAppName, NoteTakingHelper::kProdKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kNotSupported}));
}
TEST_F(NoteTakingHelperTest, ListChromeAppsWithLockScreenNotesSupported) {
Init(ENABLE_PALETTE);
ASSERT_FALSE(helper()->IsAppAvailable(profile()));
ASSERT_TRUE(helper()->GetAvailableApps(profile()).empty());
std::unique_ptr<base::Value> lock_disabled_action_handler =
extensions::ListBuilder()
.Append(app_runtime::ToString(app_runtime::ACTION_TYPE_NEW_NOTE))
.Build();
// 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(
NoteTakingHelper::kProdKeepExtensionId, kProdKeepAppName,
nullptr /* permissions */, std::move(lock_disabled_action_handler));
InstallExtension(prod_extension.get(), profile());
EXPECT_TRUE(helper()->IsAppAvailable(profile()));
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kProdKeepAppName, NoteTakingHelper::kProdKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kNotSupported}}));
EXPECT_TRUE(helper()->GetPreferredChromeAppInfo(profile()));
// Install additional Keep app - one that supports lock screen note taking.
// This app should be reported to support note taking (given that
// enable-lock-screen-apps flag is set).
scoped_refptr<const extensions::Extension> dev_extension =
CreateAndInstallLockScreenApp(NoteTakingHelper::kDevKeepExtensionId,
kDevKeepAppName, profile());
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kEnabled},
{kProdKeepAppName, NoteTakingHelper::kProdKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kNotSupported}}));
EXPECT_TRUE(helper()->GetPreferredChromeAppInfo(profile()));
}
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(NoteTakingHelper::kDevKeepExtensionId,
kDevKeepAppName, profile());
// Verify that the app is reported to support lock screen note taking.
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kEnabled}}));
EXPECT_FALSE(helper()->GetPreferredChromeAppInfo(profile()));
// 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(), NoteTakingHelper::kDevKeepExtensionId);
helper()->SetPreferredAppEnabledOnLockScreen(profile(), true);
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kEnabled}}));
EXPECT_TRUE(PreferredAppMatches(
profile(), {kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kEnabled}));
helper()->SetPreferredAppEnabledOnLockScreen(profile(), false);
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kSupported}}));
EXPECT_TRUE(PreferredAppMatches(
profile(),
{kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::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(
NoteTakingHelper::kDevKeepExtensionId, kDevKeepAppName, nullptr,
profile());
helper()->SetPreferredApp(profile(), NoteTakingHelper::kDevKeepExtensionId);
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kNotSupported}}));
EXPECT_TRUE(PreferredAppMatches(
profile(),
{kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kNotSupported}));
}
TEST_F(NoteTakingHelperTest,
PreferredAppWithotLockSupportClearsLockScreenPref) {
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(NoteTakingHelper::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, nullptr,
profile());
// Verify that only Keep app is reported to support lock screen note taking.
EXPECT_TRUE(AvailableAppsMatch(
profile(), {{kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kEnabled},
{kName, kNewNoteId, false /*preferred*/,
NoteTakingLockScreenSupport::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(), NoteTakingHelper::kDevKeepExtensionId);
helper()->SetPreferredAppEnabledOnLockScreen(profile(), true);
EXPECT_TRUE(AvailableAppsMatch(
profile(), {{kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kEnabled},
{kName, kNewNoteId, false /*preferred*/,
NoteTakingLockScreenSupport::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, NoteTakingHelper::kDevKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kEnabled},
{kName, kNewNoteId, true /*preferred*/,
NoteTakingLockScreenSupport::kNotSupported}}));
EXPECT_TRUE(PreferredAppMatches(
profile(), {kName, kNewNoteId, true /*preferred*/,
NoteTakingLockScreenSupport::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, nullptr /* permissions */,
extensions::ListBuilder()
.Append(app_runtime::ToString(app_runtime::ACTION_TYPE_NEW_NOTE))
.Build());
InstallExtension(has_new_note.get(), profile());
// "action_handlers": []
scoped_refptr<const extensions::Extension> empty_array =
CreateExtension(kEmptyArrayId, kName, nullptr /* permissions*/,
extensions::ListBuilder().Build());
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*/,
NoteTakingLockScreenSupport::kNotSupported}}));
}
// Verify that non-whitelisted 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*/,
NoteTakingLockScreenSupport::kNotSupported}}));
}
TEST_F(NoteTakingHelperTest, WhitelistedAndCustomAppsShowOnlyOnce) {
Init(ENABLE_PALETTE);
scoped_refptr<const extensions::Extension> extension = CreateExtension(
NoteTakingHelper::kProdKeepExtensionId, "Keep", nullptr /* permissions */,
extensions::ListBuilder()
.Append(app_runtime::ToString(app_runtime::ACTION_TYPE_NEW_NOTE))
.Build());
InstallExtension(extension.get(), profile());
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{"Keep", NoteTakingHelper::kProdKeepExtensionId, false /*preferred*/,
NoteTakingLockScreenSupport::kNotSupported}}));
}
TEST_F(NoteTakingHelperTest, LaunchChromeApp) {
Init(ENABLE_PALETTE);
scoped_refptr<const extensions::Extension> extension =
CreateExtension(NoteTakingHelper::kProdKeepExtensionId, "Keep");
InstallExtension(extension.get(), profile());
// Check the Chrome app is launched with the correct parameters.
HistogramTester histogram_tester;
const base::FilePath kPath("/foo/bar/photo.jpg");
helper()->LaunchAppForNewNote(profile(), kPath);
ASSERT_EQ(1u, launched_chrome_apps_.size());
EXPECT_EQ(NoteTakingHelper::kProdKeepExtensionId,
launched_chrome_apps_[0].id);
EXPECT_EQ(kPath, launched_chrome_apps_[0].path);
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(NoteTakingHelper::kProdKeepExtensionId, "prod");
InstallExtension(prod_extension.get(), profile());
scoped_refptr<const extensions::Extension> dev_extension =
CreateExtension(NoteTakingHelper::kDevKeepExtensionId, "dev");
InstallExtension(dev_extension.get(), profile());
// Set the prod app as preferred and check that it's launched.
std::unique_ptr<HistogramTester> histogram_tester(new HistogramTester());
helper()->SetPreferredApp(profile(), NoteTakingHelper::kProdKeepExtensionId);
helper()->LaunchAppForNewNote(profile(), base::FilePath());
ASSERT_EQ(1u, launched_chrome_apps_.size());
ASSERT_EQ(NoteTakingHelper::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.reset(new HistogramTester());
helper()->LaunchAppForNewNote(profile(), base::FilePath());
ASSERT_EQ(1u, launched_chrome_apps_.size());
EXPECT_EQ(NoteTakingHelper::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);
}
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();
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
const char kSecondProfileName[] = "second-profile";
auto prefs = std::make_unique<sync_preferences::TestingPrefServiceSyncable>();
RegisterUserProfilePrefs(prefs->registry());
prefs->SetBoolean(arc::prefs::kArcEnabled, true);
profile_manager()->CreateTestingProfile(
kSecondProfileName, std::move(prefs), base::ASCIIToUTF16("Second User"),
1 /* avatar_id */, std::string() /* supervised_user_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();
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*/,
NoteTakingLockScreenSupport::kNotSupported},
{kName2, kPackage2, false /*preferred*/,
NoteTakingLockScreenSupport::kNotSupported}}));
helper()->SetPreferredApp(profile(), kPackage1);
EXPECT_TRUE(AvailableAppsMatch(
profile(), {{kName1, kPackage1, true /*preferred*/,
NoteTakingLockScreenSupport::kNotSupported},
{kName2, kPackage2, false /*preferred*/,
NoteTakingLockScreenSupport::kNotSupported}}));
EXPECT_FALSE(helper()->GetPreferredChromeAppInfo(profile()));
// 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, LaunchAndroidApp) {
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(), base::FilePath());
ASSERT_EQ(1u, intent_helper_.handled_intents().size());
EXPECT_EQ(GetIntentString(kPackage1, ""),
GetIntentString(intent_helper_.handled_intents()[0]));
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();
base::RunLoop().RunUntilIdle();
helper()->SetPreferredApp(profile(), kPackage2);
// The second app should be launched now.
intent_helper_.clear_handled_intents();
histogram_tester.reset(new HistogramTester());
helper()->LaunchAppForNewNote(profile(), base::FilePath());
ASSERT_EQ(1u, intent_helper_.handled_intents().size());
EXPECT_EQ(GetIntentString(kPackage2, ""),
GetIntentString(intent_helper_.handled_intents()[0]));
histogram_tester->ExpectUniqueSample(
NoteTakingHelper::kPreferredLaunchResultHistogramName,
static_cast<int>(LaunchResult::ANDROID_SUCCESS), 1);
histogram_tester->ExpectTotalCount(
NoteTakingHelper::kDefaultLaunchResultHistogramName, 0);
}
TEST_F(NoteTakingHelperTest, LaunchAndroidAppWithPath) {
const std::string kPackage = "org.chromium.package";
std::vector<IntentHandlerInfoPtr> handlers;
handlers.emplace_back(CreateIntentHandlerInfo("App", kPackage));
intent_helper_.SetIntentHandlers(NoteTakingHelper::kIntentAction,
std::move(handlers));
Init(ENABLE_PALETTE | ENABLE_PLAY_STORE);
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(helper()->IsAppAvailable(profile()));
const base::FilePath kDownloadedPath(
file_manager::util::GetDownloadsFolderForProfile(profile()).Append(
"image.jpg"));
helper()->LaunchAppForNewNote(profile(), kDownloadedPath);
ASSERT_EQ(1u, intent_helper_.handled_intents().size());
EXPECT_EQ(GetIntentString(kPackage, GetArcUrl(kDownloadedPath)),
GetIntentString(intent_helper_.handled_intents()[0]));
const base::FilePath kRemovablePath =
base::FilePath(file_manager::util::kRemovableMediaPath)
.Append("image.jpg");
intent_helper_.clear_handled_intents();
helper()->LaunchAppForNewNote(profile(), kRemovablePath);
ASSERT_EQ(1u, intent_helper_.handled_intents().size());
EXPECT_EQ(GetIntentString(kPackage, GetArcUrl(kRemovablePath)),
GetIntentString(intent_helper_.handled_intents()[0]));
// When a path that isn't accessible to ARC is passed, the request should be
// dropped.
HistogramTester histogram_tester;
intent_helper_.clear_handled_intents();
helper()->LaunchAppForNewNote(profile(), base::FilePath("/bad/path.jpg"));
EXPECT_TRUE(intent_helper_.handled_intents().empty());
histogram_tester.ExpectUniqueSample(
NoteTakingHelper::kPreferredLaunchResultHistogramName,
static_cast<int>(LaunchResult::NO_APP_SPECIFIED), 1);
histogram_tester.ExpectUniqueSample(
NoteTakingHelper::kDefaultLaunchResultHistogramName,
static_cast<int>(LaunchResult::ANDROID_FAILED_TO_CONVERT_PATH), 1);
}
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(), base::FilePath());
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();
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(NoteTakingHelper::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-whitelisted 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();
const std::string kSecondProfileName = "second-profile";
TestingProfile* second_profile =
profile_manager()->CreateTestingProfile(kSecondProfileName);
InitExtensionService(second_profile);
EXPECT_EQ(0, observer.num_updates());
InstallExtension(keep_extension.get(), second_profile);
EXPECT_EQ(1, observer.num_updates());
UninstallExtension(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(NoteTakingHelper::kProdKeepExtensionId, "Keep");
InstallExtension(prod_keep_extension.get(), profile());
scoped_refptr<const extensions::Extension> dev_keep_extension =
CreateExtension(NoteTakingHelper::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<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<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<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.
const std::string kSecondProfileName = "second-profile";
TestingProfile* second_profile =
profile_manager()->CreateTestingProfile(kSecondProfileName);
InitExtensionService(second_profile);
scoped_refptr<const extensions::Extension>
second_profile_prod_keep_extension =
CreateExtension(NoteTakingHelper::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<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<Profile*>{second_profile},
observer.preferred_app_updates());
observer.clear_preferred_app_updates();
profile_manager()->DeleteTestingProfile(kSecondProfileName);
}
TEST_F(NoteTakingHelperTest,
NotifyObserverAboutPreferredAppLockScreenSupportChanges) {
Init(ENABLE_PALETTE);
TestObserver observer;
scoped_refptr<const extensions::Extension> dev_extension =
CreateAndInstallLockScreenApp(NoteTakingHelper::kDevKeepExtensionId,
kDevKeepAppName, profile());
scoped_refptr<const extensions::Extension> prod_extension =
CreateExtension(NoteTakingHelper::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<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<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<Profile*>{profile()}, observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// No-op, becuase 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<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(NoteTakingHelper::kDevKeepExtensionId,
kDevKeepAppName, profile());
scoped_refptr<const extensions::Extension> prod_app =
CreateAndInstallLockScreenApp(NoteTakingHelper::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, nullptr, profile());
// Disabling preffered 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<Profile*>{profile()}, observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// Verify dev app is enabled on lock screen.
EXPECT_TRUE(AvailableAppsMatch(
profile(), {{kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kEnabled},
{kProdKeepAppName, NoteTakingHelper::kProdKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kEnabled},
{kUnsupportedAppName, kUnsupportedAppId, false /*preferred*/,
NoteTakingLockScreenSupport::kNotSupported}}));
// Whitelist prod app by policy.
profile_prefs_->SetManagedPref(
prefs::kNoteTakingAppsLockScreenWhitelist,
extensions::ListBuilder().Append(prod_app->id()).Build());
// 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, NoteTakingHelper::kDevKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kNotAllowedByPolicy},
{kProdKeepAppName, NoteTakingHelper::kProdKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kEnabled},
{kUnsupportedAppName, kUnsupportedAppId, false /*preferred*/,
NoteTakingLockScreenSupport::kNotSupported}}));
// Change whitelist so only dev app is whitelisted.
profile_prefs_->SetManagedPref(
prefs::kNoteTakingAppsLockScreenWhitelist,
extensions::ListBuilder().Append(dev_app->id()).Build());
// The preferred app status changed, so observers are expected to be notified.
EXPECT_EQ(std::vector<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, NoteTakingHelper::kDevKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kEnabled},
{kProdKeepAppName, NoteTakingHelper::kProdKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kNotAllowedByPolicy},
{kUnsupportedAppName, kUnsupportedAppId, false /*preferred*/,
NoteTakingLockScreenSupport::kNotSupported}}));
// Switch preferred note taking app to one that does not support lock screen.
helper()->SetPreferredApp(profile(), unsupported_app->id());
EXPECT_EQ(std::vector<Profile*>{profile()}, observer.preferred_app_updates());
observer.clear_preferred_app_updates();
// Policy with an empty whitelist - this should disallow all apps from the
// lock screen.
profile_prefs_->SetManagedPref(prefs::kNoteTakingAppsLockScreenWhitelist,
std::make_unique<base::ListValue>());
// 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, NoteTakingHelper::kDevKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kNotAllowedByPolicy},
{kProdKeepAppName, NoteTakingHelper::kProdKeepExtensionId,
false /*preferred*/, NoteTakingLockScreenSupport::kNotAllowedByPolicy},
{kUnsupportedAppName, kUnsupportedAppId, true /*preferred*/,
NoteTakingLockScreenSupport::kNotSupported}}));
UninstallExtension(dev_app.get(), profile());
UninstallExtension(prod_app.get(), profile());
UninstallExtension(unsupported_app.get(), profile());
profile_prefs_->RemoveManagedPref(prefs::kNoteTakingAppsLockScreenWhitelist);
// No preferred app installed, so no update notification.
EXPECT_TRUE(observer.preferred_app_updates().empty());
}
TEST_F(NoteTakingHelperTest,
UpdateLockScreenSupportStatusWhenWhitelistPolicyRemoved) {
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(NoteTakingHelper::kDevKeepExtensionId,
kDevKeepAppName, profile());
helper()->SetPreferredApp(profile(), app->id());
observer.clear_preferred_app_updates();
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kEnabled}}));
// Policy with an empty whitelist - this should disallow test app from running
// on lock screen.
profile_prefs_->SetManagedPref(prefs::kNoteTakingAppsLockScreenWhitelist,
std::make_unique<base::ListValue>());
// Preferred app settings changed - observers should be notified.
EXPECT_EQ(std::vector<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, NoteTakingHelper::kDevKeepExtensionId,
true /*preferred*/,
NoteTakingLockScreenSupport::kNotAllowedByPolicy}}));
// Remove the whitelist policy - the preferred app should become enabled on
// lock screen again.
profile_prefs_->RemoveManagedPref(prefs::kNoteTakingAppsLockScreenWhitelist);
EXPECT_EQ(std::vector<Profile*>{profile()}, observer.preferred_app_updates());
observer.clear_preferred_app_updates();
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kEnabled}}));
}
TEST_F(NoteTakingHelperTest,
NoObserverCallsIfPolicyChangesBeforeLockScreenStatusIsFetched) {
Init(ENABLE_PALETTE);
TestObserver observer;
scoped_refptr<const extensions::Extension> app =
CreateAndInstallLockScreenApp(NoteTakingHelper::kDevKeepExtensionId,
kDevKeepAppName, profile());
profile_prefs_->SetManagedPref(prefs::kNoteTakingAppsLockScreenWhitelist,
std::make_unique<base::ListValue>());
// Verify that observers are not notified of preferred app change if preferred
// app is not set when whitelist 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<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::kNoteTakingAppsLockScreenWhitelist,
extensions::ListBuilder().Append(app->id()).Build());
EXPECT_TRUE(observer.preferred_app_updates().empty());
EXPECT_TRUE(AvailableAppsMatch(
profile(),
{{kDevKeepAppName, NoteTakingHelper::kDevKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kEnabled}}));
}
TEST_F(NoteTakingHelperTest, LockScreenSupportInSecondaryProfile) {
Init(ENABLE_PALETTE);
TestObserver observer;
// Initialize secondary profile.
auto prefs = std::make_unique<sync_preferences::TestingPrefServiceSyncable>();
RegisterUserProfilePrefs(prefs->registry());
sync_preferences::TestingPrefServiceSyncable* profile_prefs = prefs.get();
const std::string kSecondProfileName = "second-profile";
TestingProfile* second_profile = profile_manager()->CreateTestingProfile(
kSecondProfileName, std::move(prefs), base::ASCIIToUTF16("Test profile"),
1 /*avatar_id*/, std::string() /*supervised_user_id*/,
TestingProfile::TestingFactories());
InitExtensionService(second_profile);
// Add test apps to secondary profile.
scoped_refptr<const extensions::Extension> prod_app =
CreateAndInstallLockScreenApp(NoteTakingHelper::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, nullptr, second_profile);
// Setting preferred app should fire observers for secondary profile.
helper()->SetPreferredApp(second_profile, prod_app->id());
EXPECT_EQ(std::vector<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, NoteTakingHelper::kProdKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kNotSupported},
{kUnsupportedAppName, kUnsupportedAppId, false /*preferred*/,
NoteTakingLockScreenSupport::kNotSupported}}));
// Enabling an app on lock screen in secondary profile should fail.
EXPECT_FALSE(helper()->SetPreferredAppEnabledOnLockScreen(profile(), true));
// Policy with an empty whitelist.
profile_prefs->SetManagedPref(prefs::kNoteTakingAppsLockScreenWhitelist,
std::make_unique<base::ListValue>());
// Changing policy should not notify observers in secondary profile.
EXPECT_TRUE(observer.preferred_app_updates().empty());
EXPECT_TRUE(AvailableAppsMatch(
second_profile,
{{kProdKeepAppName, NoteTakingHelper::kProdKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kNotSupported},
{kUnsupportedAppName, kUnsupportedAppId, false /*preferred*/,
NoteTakingLockScreenSupport::kNotSupported}}));
EXPECT_TRUE(PreferredAppMatches(
second_profile,
{kProdKeepAppName, NoteTakingHelper::kProdKeepExtensionId,
true /*preferred*/, NoteTakingLockScreenSupport::kNotSupported}));
}
TEST_F(NoteTakingHelperTest, NoteTakingControllerClient) {
Init(ENABLE_PALETTE);
auto has_note_taking_apps = [&]() {
auto* client = ash::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(NoteTakingHelper::kProdKeepExtensionId, kProdKeepAppName);
scoped_refptr<const extensions::Extension> extension2 =
CreateExtension(NoteTakingHelper::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());
const std::string kSecondProfileName = "second-profile";
TestingProfile* second_profile =
profile_manager()->CreateTestingProfile(kSecondProfileName);
InitExtensionService(second_profile);
SetNoteTakingClientProfile(second_profile);
EXPECT_FALSE(has_note_taking_apps());
InstallExtension(extension2.get(), second_profile);
EXPECT_TRUE(has_note_taking_apps());
SetNoteTakingClientProfile(profile());
EXPECT_TRUE(has_note_taking_apps());
ash::NoteTakingClient::GetInstance()->CreateNote();
ASSERT_EQ(1u, launched_chrome_apps_.size());
ASSERT_EQ(NoteTakingHelper::kProdKeepExtensionId,
launched_chrome_apps_[0].id);
UninstallExtension(extension2.get(), second_profile);
EXPECT_TRUE(has_note_taking_apps());
profile_manager()->DeleteTestingProfile(kSecondProfileName);
}
} // namespace chromeos