blob: d13616e096823ce87d591265465bdf0885f58658 [file] [log] [blame]
// Copyright 2019 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/apps/app_service/app_service_proxy.h"
#include <memory>
#include <optional>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/publishers/app_publisher.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/test/base/testing_profile.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/features.h"
#include "components/services/app_service/public/cpp/icon_types.h"
#include "components/services/app_service/public/cpp/intent.h"
#include "components/services/app_service/public/cpp/intent_filter.h"
#include "components/services/app_service/public/cpp/intent_filter_util.h"
#include "components/services/app_service/public/cpp/intent_test_util.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "components/services/app_service/public/cpp/preferred_app.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image_skia_rep.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "components/services/app_service/public/cpp/types_util.h"
#endif // BUILDFLAG(IS_CHROMEOS)
namespace apps {
class FakePublisherForProxyTest : public AppPublisher {
public:
FakePublisherForProxyTest(AppServiceProxy* proxy,
AppType app_type,
std::vector<std::string> initial_app_ids)
: AppPublisher(proxy),
app_type_(app_type),
known_app_ids_(std::move(initial_app_ids)) {
RegisterPublisher(app_type_);
CallOnApps(known_app_ids_, /*uninstall=*/false);
}
FakePublisherForProxyTest(AppServiceProxy* proxy, AppType app_type)
: AppPublisher(proxy), app_type_(app_type) {
RegisterPublisher(app_type_);
}
void InitApps(std::vector<std::string> initial_app_ids) {
known_app_ids_ = std::move(initial_app_ids);
CallOnApps(known_app_ids_, /*uninstall=*/false);
}
void Launch(const std::string& app_id,
int32_t event_flags,
LaunchSource launch_source,
WindowInfoPtr window_info) override {}
void LaunchAppWithFiles(const std::string& app_id,
int32_t event_flags,
LaunchSource launch_source,
std::vector<base::FilePath> file_paths) override {}
void LaunchAppWithIntent(const std::string& app_id,
int32_t event_flags,
IntentPtr intent,
LaunchSource launch_source,
WindowInfoPtr window_info,
LaunchCallback callback) override {}
void LaunchAppWithParams(AppLaunchParams&& params,
LaunchCallback callback) override {}
void LoadIcon(const std::string& app_id,
const IconKey& icon_key,
apps::IconType icon_type,
int32_t size_hint_in_dip,
bool allow_placeholder_icon,
LoadIconCallback callback) override {}
void UninstallApps(std::vector<std::string> app_ids) {
CallOnApps(app_ids, /*uninstall=*/true);
for (const auto& app_id : app_ids) {
known_app_ids_.push_back(app_id);
}
}
bool AppHasSupportedLinksPreference(const std::string& app_id) {
return supported_link_apps_.find(app_id) != supported_link_apps_.end();
}
private:
void CallOnApps(std::vector<std::string>& app_ids, bool uninstall) {
std::vector<AppPtr> apps;
for (const auto& app_id : app_ids) {
auto app = std::make_unique<App>(app_type_, app_id);
app->readiness =
uninstall ? Readiness::kUninstalledByUser : Readiness::kReady;
apps.push_back(std::move(app));
}
AppPublisher::Publish(std::move(apps), app_type_,
/*should_notify_initialized=*/true);
}
void OnSupportedLinksPreferenceChanged(const std::string& app_id,
bool open_in_app) override {
if (open_in_app) {
supported_link_apps_.insert(app_id);
} else {
supported_link_apps_.erase(app_id);
}
}
AppType app_type_;
std::vector<std::string> known_app_ids_;
std::set<std::string> supported_link_apps_;
};
#if BUILDFLAG(IS_CHROMEOS)
// FakeAppRegistryCacheObserver is used to test OnAppUpdate.
class FakeAppRegistryCacheObserver : public apps::AppRegistryCache::Observer {
public:
explicit FakeAppRegistryCacheObserver(apps::AppRegistryCache* cache) {
app_registry_cache_observer_.Observe(cache);
}
~FakeAppRegistryCacheObserver() override = default;
// apps::AppRegistryCache::Observer overrides.
void OnAppUpdate(const apps::AppUpdate& update) override {
if (base::Contains(app_ids_, update.AppId())) {
app_ids_.erase(update.AppId());
}
if (app_ids_.empty() && !result_.IsReady()) {
result_.SetValue();
}
}
void OnAppRegistryCacheWillBeDestroyed(
apps::AppRegistryCache* cache) override {
app_registry_cache_observer_.Reset();
}
void WaitForOnAppUpdate(const std::set<std::string>& app_ids) {
app_ids_ = app_ids;
EXPECT_TRUE(result_.Wait());
}
private:
base::test::TestFuture<void> result_;
base::ScopedObservation<apps::AppRegistryCache,
apps::AppRegistryCache::Observer>
app_registry_cache_observer_{this};
std::set<std::string> app_ids_;
};
#endif // BUILDFLAG(IS_CHROMEOS)
class AppServiceProxyTest : public testing::Test {
public:
AppServiceProxyTest() = default;
void SetUp() override {
profile_ = std::make_unique<TestingProfile>();
app_service_proxy_ = AppServiceProxyFactory::GetForProfile(profile_.get());
}
protected:
using UniqueReleaser = std::unique_ptr<apps::IconLoader::Releaser>;
class FakeIconLoader : public apps::IconLoader {
public:
void FlushPendingCallbacks() {
for (auto& callback : pending_callbacks_) {
auto iv = std::make_unique<IconValue>();
iv->icon_type = IconType::kUncompressed;
iv->uncompressed =
gfx::ImageSkia(gfx::ImageSkiaRep(gfx::Size(1, 1), 1.0f));
iv->is_placeholder_icon = false;
std::move(callback).Run(std::move(iv));
num_inner_finished_callbacks_++;
}
pending_callbacks_.clear();
}
int NumInnerFinishedCallbacks() { return num_inner_finished_callbacks_; }
int NumPendingCallbacks() { return pending_callbacks_.size(); }
private:
std::unique_ptr<Releaser> LoadIconFromIconKey(
const std::string& id,
const IconKey& icon_key,
IconType icon_type,
int32_t size_hint_in_dip,
bool allow_placeholder_icon,
apps::LoadIconCallback callback) override {
if (icon_type == IconType::kUncompressed) {
pending_callbacks_.push_back(std::move(callback));
}
return nullptr;
}
int num_inner_finished_callbacks_ = 0;
std::vector<apps::LoadIconCallback> pending_callbacks_;
};
void OverrideAppServiceProxyInnerIconLoader(AppServiceProxy* proxy,
apps::IconLoader* icon_loader) {
proxy->OverrideInnerIconLoaderForTesting(icon_loader);
}
Profile* profile() { return profile_.get(); }
AppServiceProxy* proxy() { return app_service_proxy_; }
int NumOuterFinishedCallbacks() { return num_outer_finished_callbacks_; }
int num_outer_finished_callbacks_ = 0;
content::BrowserTaskEnvironment task_environment_;
std::unique_ptr<TestingProfile> profile_;
raw_ptr<AppServiceProxy> app_service_proxy_;
};
class AppServiceProxyIconTest : public AppServiceProxyTest {
protected:
UniqueReleaser LoadIcon(apps::AppServiceProxy* proxy,
const std::string& app_id) {
return proxy->LoadIcon(
app_id, IconType::kUncompressed, /*size_hint_in_dip=*/1,
/*allow_placeholder_icon=*/false,
base::BindOnce([](int* num_callbacks,
apps::IconValuePtr icon) { ++(*num_callbacks); },
&num_outer_finished_callbacks_));
}
};
TEST_F(AppServiceProxyIconTest, IconCache) {
// This is mostly a sanity check. For an isolated, comprehensive unit test of
// the IconCache code, see icon_cache_unittest.cc.
//
// This tests an AppServiceProxy as a 'black box', which uses an
// IconCache but also other IconLoader filters, such as an IconCoalescer.
FakeIconLoader fake;
OverrideAppServiceProxyInnerIconLoader(proxy(), &fake);
// The next LoadIcon call should be a cache miss.
UniqueReleaser c0 = LoadIcon(proxy(), "cromulent");
EXPECT_EQ(1, fake.NumPendingCallbacks());
EXPECT_EQ(0, fake.NumInnerFinishedCallbacks());
EXPECT_EQ(0, NumOuterFinishedCallbacks());
// After a cache miss, manually trigger the inner callback.
fake.FlushPendingCallbacks();
EXPECT_EQ(0, fake.NumPendingCallbacks());
EXPECT_EQ(1, fake.NumInnerFinishedCallbacks());
EXPECT_EQ(1, NumOuterFinishedCallbacks());
// The next LoadIcon call should be a cache hit.
UniqueReleaser c1 = LoadIcon(proxy(), "cromulent");
EXPECT_EQ(0, fake.NumPendingCallbacks());
EXPECT_EQ(1, fake.NumInnerFinishedCallbacks());
EXPECT_EQ(2, NumOuterFinishedCallbacks());
// Destroy the IconLoader::Releaser's, clearing the cache.
c0.reset();
c1.reset();
// The next LoadIcon call should be a cache miss.
UniqueReleaser c2 = LoadIcon(proxy(), "cromulent");
EXPECT_EQ(1, fake.NumPendingCallbacks());
EXPECT_EQ(1, fake.NumInnerFinishedCallbacks());
EXPECT_EQ(2, NumOuterFinishedCallbacks());
// After a cache miss, manually trigger the inner callback.
fake.FlushPendingCallbacks();
EXPECT_EQ(0, fake.NumPendingCallbacks());
EXPECT_EQ(2, fake.NumInnerFinishedCallbacks());
EXPECT_EQ(3, NumOuterFinishedCallbacks());
}
TEST_F(AppServiceProxyIconTest, IconCoalescer) {
// This is mostly a sanity check. For an isolated, comprehensive unit test of
// the IconCoalescer code, see icon_coalescer_unittest.cc.
//
// This tests an AppServiceProxy as a 'black box', which uses an
// IconCoalescer but also other IconLoader filters, such as an IconCache.
FakeIconLoader fake;
OverrideAppServiceProxyInnerIconLoader(proxy(), &fake);
// Issue 4 LoadIcon requests, 2 after de-duplication.
UniqueReleaser a0 = LoadIcon(proxy(), "avocet");
UniqueReleaser a1 = LoadIcon(proxy(), "avocet");
UniqueReleaser b2 = LoadIcon(proxy(), "brolga");
UniqueReleaser a3 = LoadIcon(proxy(), "avocet");
EXPECT_EQ(2, fake.NumPendingCallbacks());
EXPECT_EQ(0, fake.NumInnerFinishedCallbacks());
EXPECT_EQ(0, NumOuterFinishedCallbacks());
// Resolve their responses.
fake.FlushPendingCallbacks();
EXPECT_EQ(0, fake.NumPendingCallbacks());
EXPECT_EQ(2, fake.NumInnerFinishedCallbacks());
EXPECT_EQ(4, NumOuterFinishedCallbacks());
// Issue another request, that triggers neither IconCache nor IconCoalescer.
UniqueReleaser c4 = LoadIcon(proxy(), "curlew");
EXPECT_EQ(1, fake.NumPendingCallbacks());
EXPECT_EQ(2, fake.NumInnerFinishedCallbacks());
EXPECT_EQ(4, NumOuterFinishedCallbacks());
// Destroying the IconLoader::Releaser shouldn't affect the fact that there's
// an in-flight "curlew" request to the FakeIconLoader.
c4.reset();
EXPECT_EQ(1, fake.NumPendingCallbacks());
EXPECT_EQ(2, fake.NumInnerFinishedCallbacks());
EXPECT_EQ(4, NumOuterFinishedCallbacks());
// Issuing another "curlew" request should coalesce with the in-flight one.
UniqueReleaser c5 = LoadIcon(proxy(), "curlew");
EXPECT_EQ(1, fake.NumPendingCallbacks());
EXPECT_EQ(2, fake.NumInnerFinishedCallbacks());
EXPECT_EQ(4, NumOuterFinishedCallbacks());
// Resolving the in-flight request to the inner IconLoader, |fake|, should
// resolve the two coalesced requests to the outer IconLoader, |proxy|.
fake.FlushPendingCallbacks();
EXPECT_EQ(0, fake.NumPendingCallbacks());
EXPECT_EQ(3, fake.NumInnerFinishedCallbacks());
EXPECT_EQ(6, NumOuterFinishedCallbacks());
}
TEST_F(AppServiceProxyTest, ProxyAccessPerProfile) {
TestingProfile::Builder profile_builder;
// We expect an App Service in a regular profile.
auto profile = profile_builder.Build();
EXPECT_TRUE(apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(
profile.get()));
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile.get());
EXPECT_TRUE(proxy);
// We expect App Service to be unsupported in incognito.
TestingProfile::Builder incognito_builder;
auto* incognito_profile = incognito_builder.BuildIncognito(profile.get());
EXPECT_FALSE(apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(
incognito_profile));
// But if it's accidentally called, we expect the same App Service in the
// incognito profile branched from that regular profile.
// TODO(crbug.com/40146603): this should be nullptr once we address all
// incognito access to the App Service.
auto* incognito_proxy =
apps::AppServiceProxyFactory::GetForProfile(incognito_profile);
EXPECT_EQ(proxy, incognito_proxy);
// We expect a different App Service in the Guest Session profile.
TestingProfile::Builder guest_builder;
guest_builder.SetGuestSession();
auto guest_profile = guest_builder.Build();
#if BUILDFLAG(IS_CHROMEOS)
// App service is not available for original profile.
EXPECT_FALSE(apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(
guest_profile.get()));
// App service is available for OTR profile in Guest mode.
auto* guest_otr_profile =
guest_profile->GetPrimaryOTRProfile(/*create_if_needed=*/true);
EXPECT_TRUE(apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(
guest_otr_profile));
auto* guest_otr_proxy =
apps::AppServiceProxyFactory::GetForProfile(guest_otr_profile);
EXPECT_TRUE(guest_otr_proxy);
EXPECT_NE(guest_otr_proxy, proxy);
#else
EXPECT_TRUE(apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(
guest_profile.get()));
auto* guest_proxy =
apps::AppServiceProxyFactory::GetForProfile(guest_profile.get());
EXPECT_TRUE(guest_proxy);
EXPECT_NE(guest_proxy, proxy);
#endif // BUILDFLAG(IS_CHROMEOS)
}
TEST_F(AppServiceProxyTest, ReinitializeClearsCache) {
constexpr char kTestAppId[] = "pwa";
{
std::vector<AppPtr> apps;
AppPtr app = std::make_unique<App>(AppType::kWeb, kTestAppId);
apps.push_back(std::move(app));
proxy()->OnApps(std::move(apps), AppType::kWeb,
/*should_notify_initialized=*/true);
}
EXPECT_EQ(proxy()->AppRegistryCache().GetAppType(kTestAppId), AppType::kWeb);
proxy()->ReinitializeForTesting(proxy()->profile());
EXPECT_EQ(proxy()->AppRegistryCache().GetAppType(kTestAppId),
AppType::kUnknown);
}
class AppServiceProxyPreferredAppsTest : public AppServiceProxyTest {
public:
void SetUp() override {
AppServiceProxyTest::SetUp();
// Wait for the PreferredAppsList to be initialized from disk before tests
// start modifying it.
base::RunLoop file_read_run_loop;
proxy()->ReinitializeForTesting(profile(),
file_read_run_loop.QuitClosure());
file_read_run_loop.Run();
web_app::test::AwaitStartWebAppProviderAndSubsystems(profile());
}
// Shortcut for adding apps to App Service without going through a real
// Publisher.
void OnApps(std::vector<AppPtr> apps, AppType type) {
proxy()->OnApps(std::move(apps), type,
/*should_notify_initialized=*/false);
}
PreferredAppsList& GetPreferredAppsList() {
return proxy()->preferred_apps_impl_->preferred_apps_list_;
}
PreferredAppsImpl* PreferredAppsImpl() {
return proxy()->preferred_apps_impl_.get();
}
};
TEST_F(AppServiceProxyPreferredAppsTest, UpdatedOnUninstall) {
constexpr char kTestAppId[] = "foo";
const GURL kTestUrl = GURL("https://www.example.com/");
// Install an app and set it as preferred for a URL.
{
std::vector<AppPtr> apps;
AppPtr app = std::make_unique<App>(AppType::kWeb, kTestAppId);
app->readiness = Readiness::kReady;
app->intent_filters.push_back(
apps_util::MakeIntentFilterForUrlScope(kTestUrl));
apps.push_back(std::move(app));
OnApps(std::move(apps), AppType::kWeb);
proxy()->SetSupportedLinksPreference(kTestAppId);
std::optional<std::string> preferred_app =
proxy()->PreferredAppsList().FindPreferredAppForUrl(kTestUrl);
ASSERT_EQ(kTestAppId, preferred_app);
}
// Updating the app should not change its preferred app status.
{
std::vector<AppPtr> apps;
AppPtr app = std::make_unique<App>(AppType::kWeb, kTestAppId);
app->last_launch_time = base::Time();
apps.push_back(std::move(app));
OnApps(std::move(apps), AppType::kWeb);
std::optional<std::string> preferred_app =
proxy()->PreferredAppsList().FindPreferredAppForUrl(kTestUrl);
ASSERT_EQ(kTestAppId, preferred_app);
}
// Uninstalling the app should remove it from the preferred app list.
{
std::vector<AppPtr> apps;
AppPtr app = std::make_unique<App>(AppType::kWeb, kTestAppId);
app->readiness = Readiness::kUninstalledByUser;
apps.push_back(std::move(app));
OnApps(std::move(apps), AppType::kWeb);
std::optional<std::string> preferred_app =
proxy()->PreferredAppsList().FindPreferredAppForUrl(kTestUrl);
ASSERT_EQ(std::nullopt, preferred_app);
}
}
TEST_F(AppServiceProxyPreferredAppsTest, SetPreferredApp) {
constexpr char kTestAppId1[] = "abc";
constexpr char kTestAppId2[] = "def";
const GURL kTestUrl1 = GURL("https://www.foo.com/");
const GURL kTestUrl2 = GURL("https://www.bar.com/");
auto url_filter_1 = apps_util::MakeIntentFilterForUrlScope(kTestUrl1);
auto url_filter_2 = apps_util::MakeIntentFilterForUrlScope(kTestUrl2);
auto send_filter = apps_util::MakeIntentFilterForSend("image/png");
std::vector<AppPtr> apps;
AppPtr app1 = std::make_unique<App>(AppType::kWeb, kTestAppId1);
app1->readiness = Readiness::kReady;
app1->intent_filters.push_back(url_filter_1->Clone());
app1->intent_filters.push_back(url_filter_2->Clone());
app1->intent_filters.push_back(send_filter->Clone());
apps.push_back(std::move(app1));
AppPtr app2 = std::make_unique<App>(AppType::kWeb, kTestAppId2);
app2->readiness = Readiness::kReady;
app2->intent_filters.push_back(url_filter_1->Clone());
apps.push_back(std::move(app2));
OnApps(std::move(apps), AppType::kWeb);
// Set app 1 as preferred. Both links should be set as preferred, but the
// non-link filter is ignored.
proxy()->SetSupportedLinksPreference(kTestAppId1);
ASSERT_EQ(kTestAppId1,
proxy()->PreferredAppsList().FindPreferredAppForUrl(kTestUrl1));
ASSERT_EQ(kTestAppId1,
proxy()->PreferredAppsList().FindPreferredAppForUrl(kTestUrl2));
auto mime_intent = std::make_unique<Intent>(apps_util::kIntentActionSend);
mime_intent->mime_type = "image/png";
ASSERT_EQ(
std::nullopt,
proxy()->PreferredAppsList().FindPreferredAppForIntent(mime_intent));
// Set app 2 as preferred. Both of the previous preferences for app 1 should
// be removed.
proxy()->SetSupportedLinksPreference(kTestAppId2);
ASSERT_EQ(kTestAppId2,
proxy()->PreferredAppsList().FindPreferredAppForUrl(kTestUrl1));
ASSERT_EQ(std::nullopt,
proxy()->PreferredAppsList().FindPreferredAppForUrl(kTestUrl2));
// Remove all supported link preferences for app 2.
proxy()->RemoveSupportedLinksPreference(kTestAppId2);
ASSERT_EQ(std::nullopt,
proxy()->PreferredAppsList().FindPreferredAppForUrl(kTestUrl1));
}
// Tests that writing a preferred app value before the PreferredAppsList is
// initialized queues the write for after initialization.
TEST_F(AppServiceProxyPreferredAppsTest, PreferredAppsWriteBeforeInit) {
base::RunLoop run_loop_read;
proxy()->ReinitializeForTesting(proxy()->profile(),
run_loop_read.QuitClosure());
GURL filter_url1("https://www.abc.com/");
GURL filter_url2("https://www.def.com/");
std::string kAppId1 = "aaa";
std::string kAppId2 = "bbb";
IntentFilters filters1;
filters1.push_back(apps_util::MakeIntentFilterForUrlScope(filter_url1));
proxy()->SetSupportedLinksPreference(kAppId1, std::move(filters1));
IntentFilters filters2;
filters2.push_back(apps_util::MakeIntentFilterForUrlScope(filter_url2));
proxy()->SetSupportedLinksPreference(kAppId2, std::move(filters2));
// Wait for the preferred apps list initialization to read from disk.
run_loop_read.Run();
// Both changes to the PreferredAppsList should have been applied.
ASSERT_EQ(kAppId1,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url1));
ASSERT_EQ(kAppId2,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url2));
}
TEST_F(AppServiceProxyPreferredAppsTest, PreferredAppsPersistency) {
const char kAppId1[] = "abcdefg";
GURL filter_url = GURL("https://www.google.com/abc");
auto intent_filter = apps_util::MakeIntentFilterForUrlScope(filter_url);
{
base::RunLoop run_loop_read;
base::RunLoop run_loop_write;
proxy()->ReinitializeForTesting(proxy()->profile(),
run_loop_read.QuitClosure(),
run_loop_write.QuitClosure());
run_loop_read.Run();
IntentFilters filters;
filters.push_back(apps_util::MakeIntentFilterForUrlScope(filter_url));
proxy()->SetSupportedLinksPreference(kAppId1, std::move(filters));
run_loop_write.Run();
}
// Create a new impl to initialize preferred apps from the disk.
{
base::RunLoop run_loop_read;
proxy()->ReinitializeForTesting(proxy()->profile(),
run_loop_read.QuitClosure());
run_loop_read.Run();
EXPECT_EQ(kAppId1,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url));
}
}
TEST_F(AppServiceProxyPreferredAppsTest,
PreferredAppsSetSupportedLinksPublisher) {
GetPreferredAppsList().Init();
const char kAppId1[] = "abcdefg";
const char kAppId2[] = "hijklmn";
const char kAppId3[] = "opqrstu";
auto intent_filter_a =
apps_util::MakeIntentFilterForUrlScope(GURL("https://www.a.com/"));
auto intent_filter_b =
apps_util::MakeIntentFilterForUrlScope(GURL("https://www.b.com/"));
auto intent_filter_c =
apps_util::MakeIntentFilterForUrlScope(GURL("https://www.c.com/"));
FakePublisherForProxyTest pub(
proxy(), AppType::kArc,
std::vector<std::string>{kAppId1, kAppId2, kAppId3});
IntentFilters app_1_filters;
app_1_filters.push_back(intent_filter_a->Clone());
app_1_filters.push_back(intent_filter_b->Clone());
proxy()->SetSupportedLinksPreference(kAppId1, std::move(app_1_filters));
IntentFilters app_2_filters;
app_2_filters.push_back(intent_filter_c->Clone());
proxy()->SetSupportedLinksPreference(kAppId2, std::move(app_2_filters));
EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId1));
EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId2));
EXPECT_FALSE(pub.AppHasSupportedLinksPreference(kAppId3));
EXPECT_EQ(kAppId1, GetPreferredAppsList().FindPreferredAppForUrl(
GURL("https://www.a.com/")));
EXPECT_EQ(kAppId1, GetPreferredAppsList().FindPreferredAppForUrl(
GURL("https://www.b.com/")));
EXPECT_EQ(kAppId2, GetPreferredAppsList().FindPreferredAppForUrl(
GURL("https://www.c.com/")));
// App 3 overlaps with both App 1 and 2. Both previous apps should have all
// their supported link filters removed.
IntentFilters app_3_filters;
app_3_filters.push_back(intent_filter_b->Clone());
app_3_filters.push_back(intent_filter_c->Clone());
proxy()->SetSupportedLinksPreference(kAppId3, std::move(app_3_filters));
EXPECT_FALSE(pub.AppHasSupportedLinksPreference(kAppId1));
EXPECT_FALSE(pub.AppHasSupportedLinksPreference(kAppId2));
EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId3));
EXPECT_EQ(std::nullopt, GetPreferredAppsList().FindPreferredAppForUrl(
GURL("https://www.a.com/")));
EXPECT_EQ(kAppId3, GetPreferredAppsList().FindPreferredAppForUrl(
GURL("https://www.b.com/")));
EXPECT_EQ(kAppId3, GetPreferredAppsList().FindPreferredAppForUrl(
GURL("https://www.c.com/")));
// Setting App 3 as preferred again should not change anything.
app_3_filters = std::vector<IntentFilterPtr>();
app_3_filters.push_back(intent_filter_b->Clone());
app_3_filters.push_back(intent_filter_c->Clone());
proxy()->SetSupportedLinksPreference(kAppId3, std::move(app_3_filters));
EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId3));
proxy()->RemoveSupportedLinksPreference(kAppId3);
EXPECT_FALSE(pub.AppHasSupportedLinksPreference(kAppId3));
}
// Test that app with overlapped supported links works properly.
TEST_F(AppServiceProxyPreferredAppsTest, PreferredAppsOverlapSupportedLink) {
// Test Initialize.
GetPreferredAppsList().Init();
const char kAppId1[] = "abcdefg";
const char kAppId2[] = "hijklmn";
GURL filter_url_1 = GURL("https://www.google.com/abc");
GURL filter_url_2 = GURL("http://www.google.com.au/abc");
GURL filter_url_3 = GURL("https://www.abc.com/abc");
auto intent_filter_1 = apps_util::MakeIntentFilterForUrlScope(filter_url_1);
apps_util::AddConditionValue(ConditionType::kScheme, filter_url_2.scheme(),
PatternMatchType::kLiteral, intent_filter_1);
apps_util::AddConditionValue(ConditionType::kAuthority, filter_url_2.host(),
PatternMatchType::kLiteral, intent_filter_1);
auto intent_filter_2 = apps_util::MakeIntentFilterForUrlScope(filter_url_3);
apps_util::AddConditionValue(ConditionType::kScheme, filter_url_2.scheme(),
PatternMatchType::kLiteral, intent_filter_2);
apps_util::AddConditionValue(ConditionType::kAuthority, filter_url_2.host(),
PatternMatchType::kLiteral, intent_filter_2);
auto intent_filter_3 = apps_util::MakeIntentFilterForUrlScope(filter_url_1);
IntentFilters app_1_filters;
app_1_filters.push_back(std::move(intent_filter_1));
app_1_filters.push_back(std::move(intent_filter_2));
IntentFilters app_2_filters;
app_2_filters.push_back(std::move(intent_filter_3));
FakePublisherForProxyTest pub(proxy(), AppType::kArc,
std::vector<std::string>{kAppId1, kAppId2});
EXPECT_EQ(std::nullopt,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_1));
EXPECT_EQ(std::nullopt,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_2));
EXPECT_EQ(std::nullopt,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_3));
EXPECT_EQ(0U, GetPreferredAppsList().GetEntrySize());
// Test that add preferred app with overlapped filters for same app will
// add all entries.
proxy()->SetSupportedLinksPreference(kAppId1,
CloneIntentFilters(app_1_filters));
EXPECT_EQ(kAppId1,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_1));
EXPECT_EQ(kAppId1,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_2));
EXPECT_EQ(kAppId1,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_3));
EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId1));
EXPECT_FALSE(pub.AppHasSupportedLinksPreference(kAppId2));
EXPECT_EQ(2U, GetPreferredAppsList().GetEntrySize());
// Test that add preferred app with another app that has overlapped filter
// will clear all entries from the original app.
proxy()->SetSupportedLinksPreference(kAppId2,
CloneIntentFilters(app_2_filters));
EXPECT_EQ(kAppId2,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_1));
EXPECT_EQ(std::nullopt,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_2));
EXPECT_EQ(std::nullopt,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_3));
EXPECT_FALSE(pub.AppHasSupportedLinksPreference(kAppId1));
EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId2));
EXPECT_EQ(1U, GetPreferredAppsList().GetEntrySize());
// Test that setting back to app 1 works.
proxy()->SetSupportedLinksPreference(kAppId1,
CloneIntentFilters(app_1_filters));
EXPECT_EQ(kAppId1,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_1));
EXPECT_EQ(kAppId1,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_2));
EXPECT_EQ(kAppId1,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_3));
EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId1));
EXPECT_FALSE(pub.AppHasSupportedLinksPreference(kAppId2));
EXPECT_EQ(2U, GetPreferredAppsList().GetEntrySize());
}
// Test that duplicated entry will not be added for supported links.
TEST_F(AppServiceProxyPreferredAppsTest, PreferredAppsDuplicatedSupportedLink) {
// Test Initialize.
GetPreferredAppsList().Init();
const char kAppId1[] = "abcdefg";
GURL filter_url_1 = GURL("https://www.google.com/abc");
GURL filter_url_2 = GURL("http://www.google.com.au/abc");
GURL filter_url_3 = GURL("https://www.abc.com/abc");
auto intent_filter_1 = apps_util::MakeIntentFilterForUrlScope(filter_url_1);
auto intent_filter_2 = apps_util::MakeIntentFilterForUrlScope(filter_url_2);
auto intent_filter_3 = apps_util::MakeIntentFilterForUrlScope(filter_url_3);
IntentFilters app_1_filters;
app_1_filters.push_back(std::move(intent_filter_1));
app_1_filters.push_back(std::move(intent_filter_2));
app_1_filters.push_back(std::move(intent_filter_3));
FakePublisherForProxyTest pub(proxy(), AppType::kArc,
std::vector<std::string>{kAppId1});
EXPECT_EQ(std::nullopt,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_1));
EXPECT_EQ(std::nullopt,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_2));
EXPECT_EQ(std::nullopt,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_3));
EXPECT_EQ(0U, GetPreferredAppsList().GetEntrySize());
proxy()->SetSupportedLinksPreference(kAppId1,
CloneIntentFilters(app_1_filters));
EXPECT_EQ(kAppId1,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_1));
EXPECT_EQ(kAppId1,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_2));
EXPECT_EQ(kAppId1,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_3));
EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId1));
EXPECT_EQ(3U, GetPreferredAppsList().GetEntrySize());
proxy()->SetSupportedLinksPreference(kAppId1,
CloneIntentFilters(app_1_filters));
EXPECT_EQ(kAppId1,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_1));
EXPECT_EQ(kAppId1,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_2));
EXPECT_EQ(kAppId1,
GetPreferredAppsList().FindPreferredAppForUrl(filter_url_3));
EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId1));
EXPECT_EQ(3U, GetPreferredAppsList().GetEntrySize());
}
#if BUILDFLAG(IS_CHROMEOS)
TEST_F(AppServiceProxyPreferredAppsTest, PreferredAppsSetSupportedLinks) {
GetPreferredAppsList().Init();
const char kAppId1[] = "abcdefg";
const char kAppId2[] = "hijklmn";
const char kAppId3[] = "opqrstu";
auto intent_filter_a =
apps_util::MakeIntentFilterForUrlScope(GURL("https://www.a.com/"));
auto intent_filter_b =
apps_util::MakeIntentFilterForUrlScope(GURL("https://www.b.com/"));
auto intent_filter_c =
apps_util::MakeIntentFilterForUrlScope(GURL("https://www.c.com/"));
FakePublisherForProxyTest pub(
proxy(), AppType::kArc,
std::vector<std::string>{kAppId1, kAppId2, kAppId3});
IntentFilters app_1_filters;
app_1_filters.push_back(intent_filter_a->Clone());
app_1_filters.push_back(intent_filter_b->Clone());
proxy()->SetSupportedLinksPreference(kAppId1, std::move(app_1_filters));
IntentFilters app_2_filters;
app_2_filters.push_back(intent_filter_c->Clone());
proxy()->SetSupportedLinksPreference(kAppId2, std::move(app_2_filters));
EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId1));
EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId2));
EXPECT_FALSE(pub.AppHasSupportedLinksPreference(kAppId3));
// App 3 overlaps with both App 1 and 2. Both previous apps should have all
// their supported link filters removed.
IntentFilters app_3_filters;
app_3_filters.push_back(intent_filter_b->Clone());
app_3_filters.push_back(intent_filter_c->Clone());
proxy()->SetSupportedLinksPreference(kAppId3, std::move(app_3_filters));
EXPECT_FALSE(pub.AppHasSupportedLinksPreference(kAppId1));
EXPECT_FALSE(pub.AppHasSupportedLinksPreference(kAppId2));
EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId3));
// Setting App 3 as preferred again should not change anything.
app_3_filters = std::vector<IntentFilterPtr>();
app_3_filters.push_back(intent_filter_b->Clone());
app_3_filters.push_back(intent_filter_c->Clone());
proxy()->SetSupportedLinksPreference(kAppId3, std::move(app_3_filters));
EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId3));
proxy()->RemoveSupportedLinksPreference(kAppId3);
EXPECT_FALSE(pub.AppHasSupportedLinksPreference(kAppId3));
}
TEST_F(AppServiceProxyTest, LaunchCallback) {
bool called_1 = false;
bool called_2 = false;
auto instance_id_1 = base::UnguessableToken::Create();
auto instance_id_2 = base::UnguessableToken::Create();
// If the instance is not created yet, the callback will be stored.
{
LaunchResult result_1;
result_1.instance_ids.push_back(instance_id_1);
proxy()->OnLaunched(
base::BindOnce(
[](bool* called, apps::LaunchResult&& launch_result) {
*called = true;
},
&called_1),
std::move(result_1));
}
EXPECT_EQ(proxy()->callback_list_.size(), 1U);
EXPECT_FALSE(called_1);
{
LaunchResult result_2;
result_2.instance_ids.push_back(instance_id_2);
proxy()->OnLaunched(
base::BindOnce(
[](bool* called, apps::LaunchResult&& launch_result) {
*called = true;
},
&called_2),
std::move(result_2));
}
EXPECT_EQ(proxy()->callback_list_.size(), 2U);
EXPECT_FALSE(called_2);
// Once the instance is created, the callback will be called.
{
auto delta =
std::make_unique<apps::Instance>("abc", instance_id_1, nullptr);
proxy()->InstanceRegistry().OnInstance(std::move(delta));
}
EXPECT_EQ(proxy()->callback_list_.size(), 1U);
EXPECT_TRUE(called_1);
EXPECT_FALSE(called_2);
// New callback with existing instance will be called immediately.
called_1 = false;
{
LaunchResult result_3;
proxy()->OnLaunched(
base::BindOnce(
[](bool* called, apps::LaunchResult&& launch_result) {
*called = true;
},
&called_1),
std::move(result_3));
}
EXPECT_EQ(proxy()->callback_list_.size(), 1U);
EXPECT_TRUE(called_1);
EXPECT_FALSE(called_2);
// A launch that results in multiple instances.
auto instance_id_3 = base::UnguessableToken::Create();
auto instance_id_4 = base::UnguessableToken::Create();
bool called_multi = false;
{
LaunchResult result_multi;
result_multi.instance_ids.push_back(instance_id_3);
result_multi.instance_ids.push_back(instance_id_4);
proxy()->OnLaunched(
base::BindOnce(
[](bool* called, apps::LaunchResult&& launch_result) {
*called = true;
},
&called_multi),
std::move(result_multi));
}
EXPECT_EQ(proxy()->callback_list_.size(), 2U);
EXPECT_FALSE(called_multi);
proxy()->InstanceRegistry().OnInstance(
std::make_unique<apps::Instance>("foo", instance_id_3, nullptr));
proxy()->InstanceRegistry().OnInstance(
std::make_unique<apps::Instance>("bar", instance_id_4, nullptr));
EXPECT_EQ(proxy()->callback_list_.size(), 1U);
EXPECT_TRUE(called_multi);
}
TEST_F(AppServiceProxyTest, GetAppsForIntentBestHandler) {
const char kAppId1[] = "abcdefg";
const GURL kTestUrl = GURL("https://www.example.com/");
std::vector<AppPtr> apps;
// A scheme-only filter that will be excluded by the |exclude_browsers|
// parameter.
AppPtr app = std::make_unique<App>(AppType::kWeb, kAppId1);
app->readiness = Readiness::kReady;
app->handles_intents = true;
auto intent_filter = std::make_unique<apps::IntentFilter>();
intent_filter->AddSingleValueCondition(apps::ConditionType::kScheme,
kTestUrl.scheme(),
apps::PatternMatchType::kLiteral);
intent_filter->activity_name = "name 1";
intent_filter->activity_label = "same label";
app->intent_filters.push_back(std::move(intent_filter));
// A regular mime type file filter which we expect to match.
auto intent_filter2 = std::make_unique<apps::IntentFilter>();
intent_filter2->AddSingleValueCondition(apps::ConditionType::kAction,
apps_util::kIntentActionView,
apps::PatternMatchType::kLiteral);
intent_filter2->AddSingleValueCondition(apps::ConditionType::kFile,
"text/plain",
apps::PatternMatchType::kMimeType);
intent_filter2->activity_name = "name 2";
intent_filter2->activity_label = "same label";
app->intent_filters.push_back(std::move(intent_filter2));
apps.push_back(std::move(app));
proxy()->OnApps(std::move(apps), AppType::kWeb, false);
std::vector<apps::IntentFilePtr> files;
auto file = std::make_unique<apps::IntentFile>(GURL("abc.txt"));
file->mime_type = "text/plain";
file->is_directory = false;
files.push_back(std::move(file));
apps::IntentPtr intent = std::make_unique<apps::Intent>(
apps_util::kIntentActionView, std::move(files));
std::vector<apps::IntentLaunchInfo> intent_launch_info =
proxy()->GetAppsForIntent(intent, /*exclude_browsers=*/true);
// Check that we actually get back the 2nd filter, and not the excluded
// scheme-only filter which should have been discarded.
EXPECT_EQ(1U, intent_launch_info.size());
EXPECT_EQ("name 2", intent_launch_info[0].activity_name);
}
#endif // BUILDFLAG(IS_CHROMEOS)
} // namespace apps