910e62b5创建于 1月15日历史提交
// 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/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/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 "chromeos/ash/experiences/arc/arc_prefs.h"
#include "chromeos/ash/experiences/arc/mojom/file_system.mojom.h"
#include "chromeos/ash/experiences/arc/mojom/intent_common.mojom.h"
#include "chromeos/ash/experiences/arc/mojom/intent_helper.mojom.h"
#include "chromeos/ash/experiences/arc/session/arc_bridge_service.h"
#include "chromeos/ash/experiences/arc/session/arc_service_manager.h"
#include "chromeos/ash/experiences/arc/session/connection_holder.h"
#include "chromeos/ash/experiences/arc/test/connection_holder_util.h"
#include "chromeos/ash/experiences/arc/test/fake_file_system_instance.h"
#include "chromeos/ash/experiences/arc/test/fake_intent_helper_host.h"
#include "chromeos/ash/experiences/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 "components/user_manager/test_helper.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "extensions/browser/extension_registrar.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 "google_apis/gaia/gaia_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 {

auto& kDevKeepExtensionId = NoteTakingHelper::kDevKeepExtensionId;
auto& kProdKeepExtensionId = NoteTakingHelper::kProdKeepExtensionId;

// Name of default profile.
constexpr char kTestProfileName[] = "test-profile";
constexpr char kSecondProfileName[] = "second-profile";
constexpr GaiaId::Literal kFakeGaia2("fakegaia2");

// Names for keep apps used in tests.
constexpr char kProdKeepAppName[] = "Google Keep [prod]";
constexpr char kDevKeepAppName[] = "Google Keep [dev]";

std::string GetAppString(const std::string& name,
                         const std::string& id,
                         bool preferred) {
  return base::StringPrintf("{%s, %s, %d}", name.c_str(), id.c_str(),
                            preferred);
}
std::string GetAppString(const NoteTakingAppInfo& info) {
  return GetAppString(info.name, info.app_id, info.preferred);
}

// 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);

    arc_app_test_.PreProfileSetUp();

    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_app_test_.PreProfileTearDown();
    }
    BrowserWithTestWindowTest::TearDown();
    arc_app_test_.PostProfileTearDown();
    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(); }

  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_app_test_.PostProfileSetUp(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::ExtensionRegistrar::Get(profile)->AddExtension(extension);
  }
  void UninstallExtension(const extensions::Extension* extension,
                          Profile* profile) {
    std::u16string error;
    extensions::ExtensionRegistrar::Get(profile)->UninstallExtension(
        extension->id(),
        extensions::UninstallReason::UNINSTALL_REASON_FOR_TESTING, &error);
  }

  // BrowserWithTestWindowTest:
  std::optional<std::string> GetDefaultProfileName() override {
    return kTestProfileName;
  }

  // TODO(crbug.com/40286020): merge into BrowserWithTestWindowTest.
  void LogIn(std::string_view email, const GaiaId& gaia_id) override {
    AccountId account_id = AccountId::FromUserEmailGaiaId(email, gaia_id);
    user_manager()->AddGaiaUser(account_id, user_manager::UserType::kRegular);
    user_manager()->UserLoggedIn(
        account_id, user_manager::TestHelper::GetFakeUsernameHash(account_id));
  }

  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());
    return profile;
  }

  TestingProfile* CreateAndInitSecondaryProfile() {
    auto prefs =
        std::make_unique<sync_preferences::TestingPrefServiceSyncable>();
    RegisterUserProfilePrefs(prefs->registry());
    const AccountId account_id(
        AccountId::FromUserEmailGaiaId(kSecondProfileName, kFakeGaia2));
    user_manager()->AddGaiaUser(account_id, user_manager::UserType::kRegular);
    TestingProfile* profile = profile_manager()->CreateTestingProfile(
        kSecondProfileName, std::move(prefs), u"second-profile-username",
        /*avatar_id=*/1, TestingProfile::TestingFactories());

    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) {
    EXPECT_EQ(profile(), passed_context);
    launched_chrome_apps_.push_back(ChromeAppLaunchInfo{extension->id()});
  }

  // Has Init() been called?
  bool initialized_ = false;

  ArcAppTest arc_app_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*/}}));

  // 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*/},
       {kProdKeepAppName, kProdKeepExtensionId, false /*preferred*/}}));
  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*/},
       {kProdKeepAppName, kProdKeepExtensionId, false /*preferred*/}}));
  EXPECT_TRUE(helper()->GetPreferredAppId(profile()).empty());

  // Mark the prod version as preferred.
  helper()->SetPreferredApp(profile(), kProdKeepExtensionId);
  EXPECT_TRUE(AvailableAppsMatch(
      profile(),
      {{kDevKeepAppName, kDevKeepExtensionId, false /*preferred*/},
       {kProdKeepAppName, kProdKeepExtensionId, true /*preferred*/}}));
  EXPECT_EQ(helper()->GetPreferredAppId(profile()), kProdKeepExtensionId);
}

// 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*/}}));
}

// 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},
                  {"Web App 2", app2_id, /*preferred=*/false}}));
}

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},
                  {"Web App 2", app2_id, /*preferred=*/false}}));
}

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());
}

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*/},
                                  {kName2, kPackage2, false /*preferred*/}}));

  helper()->SetPreferredApp(profile(), kPackage1);

  EXPECT_TRUE(AvailableAppsMatch(profile(),
                                 {{kName1, kPackage1, true /*preferred*/},
                                  {kName2, kPackage2, false /*preferred*/}}));
  // Preferred app is not actually installed, so no app ID should be returned.
  EXPECT_TRUE(helper()->GetPreferredAppId(profile()).empty());

  // 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());
}

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();
}

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());
  }
}

}  // namespace ash