910e62b5创建于 1月15日历史提交
// 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 {

#if BUILDFLAG(IS_CHROMEOS)
apps::IntentFilterPtr CreateIntentFilterForProtocolScheme(
    const std::string& protocol_scheme) {
  auto intent_filter = std::make_unique<apps::IntentFilter>();
  intent_filter->AddSingleValueCondition(apps::ConditionType::kAction,
                                         apps_util::kIntentActionView,
                                         apps::PatternMatchType::kLiteral);
  intent_filter->AddSingleValueCondition(apps::ConditionType::kScheme,
                                         protocol_scheme,
                                         apps::PatternMatchType::kLiteral);
  return intent_filter;
}
#endif

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

#if BUILDFLAG(IS_CHROMEOS)
TEST_F(AppServiceProxyPreferredAppsTest, SetProtocolLinkPreference) {
  constexpr char kTestAppId1[] = "abc";
  constexpr char kTestAppId2[] = "def";
  const GURL kTestUrl1 = GURL("web+meow://something");

  auto protocol_link_filter = CreateIntentFilterForProtocolScheme("web+meow");

  {
    std::vector<AppPtr> apps;
    AppPtr app1 = std::make_unique<App>(AppType::kWeb, kTestAppId1);
    app1->readiness = Readiness::kReady;
    app1->intent_filters.emplace().push_back(protocol_link_filter->Clone());
    apps.push_back(std::move(app1));

    AppPtr app2 = std::make_unique<App>(AppType::kWeb, kTestAppId2);
    app2->readiness = Readiness::kReady;
    app2->intent_filters.emplace().push_back(protocol_link_filter->Clone());
    apps.push_back(std::move(app2));

    OnApps(std::move(apps), AppType::kWeb);
  }

  proxy()->SetProtocolLinkPreference(kTestAppId1, "web+meow");
  ASSERT_EQ(kTestAppId1,
            proxy()->PreferredAppsList().FindPreferredAppForUrl(kTestUrl1));

  proxy()->SetProtocolLinkPreference(kTestAppId2, "web+meow");
  ASSERT_EQ(kTestAppId2,
            proxy()->PreferredAppsList().FindPreferredAppForUrl(kTestUrl1));

  // Revoke app2's ability to handle protocol links.
  {
    std::vector<AppPtr> apps;

    AppPtr app2 = std::make_unique<App>(AppType::kWeb, kTestAppId2);
    app2->readiness = Readiness::kReady;
    app2->intent_filters.emplace();
    apps.push_back(std::move(app2));

    OnApps(std::move(apps), AppType::kWeb);
  }

  ASSERT_EQ(std::nullopt,
            proxy()->PreferredAppsList().FindPreferredAppForUrl(kTestUrl1));
}

TEST_F(AppServiceProxyPreferredAppsTest, SetProtocolLinkPreferenceBeforeInit) {
  base::RunLoop run_loop_read;
  proxy()->ReinitializeForTesting(proxy()->profile(),
                                  run_loop_read.QuitClosure());

  constexpr char kTestAppId1[] = "abc";
  const GURL kTestUrl1 = GURL("web+meow://something");

  std::vector<AppPtr> apps;
  AppPtr app1 = std::make_unique<App>(AppType::kWeb, kTestAppId1);
  app1->readiness = Readiness::kReady;
  app1->intent_filters.emplace().push_back(
      CreateIntentFilterForProtocolScheme("web+meow"));
  apps.push_back(std::move(app1));

  OnApps(std::move(apps), AppType::kWeb);
  proxy()->SetProtocolLinkPreference(kTestAppId1, "web+meow");

  // Wait for the preferred apps list initialization to read from disk.
  run_loop_read.Run();

  ASSERT_EQ(kTestAppId1,
            proxy()->PreferredAppsList().FindPreferredAppForUrl(kTestUrl1));
}

TEST_F(AppServiceProxyPreferredAppsTest, SetProtocolLinkPreferencePersistence) {
  constexpr char kTestAppId1[] = "abc";
  const GURL kTestUrl1 = GURL("web+meow://something");

  {
    base::RunLoop run_loop_read;
    base::RunLoop run_loop_write;
    proxy()->ReinitializeForTesting(proxy()->profile(),
                                    run_loop_read.QuitClosure(),
                                    run_loop_write.QuitClosure());
    std::vector<AppPtr> apps;
    AppPtr app1 = std::make_unique<App>(AppType::kWeb, kTestAppId1);
    app1->readiness = Readiness::kReady;
    app1->intent_filters.emplace().push_back(
        CreateIntentFilterForProtocolScheme("web+meow"));
    apps.push_back(std::move(app1));

    OnApps(std::move(apps), AppType::kWeb);
    proxy()->SetProtocolLinkPreference(kTestAppId1, "web+meow");
    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(kTestAppId1,
              GetPreferredAppsList().FindPreferredAppForUrl(kTestUrl1));
  }
}

#endif

// 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.GetScheme(),
                               PatternMatchType::kLiteral, intent_filter_1);
  apps_util::AddConditionValue(ConditionType::kAuthority,
                               filter_url_2.GetHost(),
                               PatternMatchType::kLiteral, intent_filter_1);

  auto intent_filter_2 = apps_util::MakeIntentFilterForUrlScope(filter_url_3);
  apps_util::AddConditionValue(ConditionType::kScheme, filter_url_2.GetScheme(),
                               PatternMatchType::kLiteral, intent_filter_2);
  apps_util::AddConditionValue(ConditionType::kAuthority,
                               filter_url_2.GetHost(),
                               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.GetScheme(),
                                         apps::PatternMatchType::kLiteral);
  intent_filter->activity_name = "name 1";
  intent_filter->activity_label = "same label";
  app->intent_filters.emplace();
  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