chromium/chrome/browser/ash/apps/apk_web_app_service_lacros_browsertest.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <iterator>

#include "ash/components/arc/mojom/app.mojom-forward.h"
#include "ash/components/arc/mojom/app.mojom.h"
#include "ash/components/arc/test/arc_util_test_support.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/shelf_model.h"
#include "base/callback_list.h"
#include "base/functional/bind.h"
#include "base/test/scoped_command_line.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.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_list_prefs.h"
#include "chrome/browser/ash/app_list/arc/arc_app_list_prefs_factory.h"
#include "chrome/browser/ash/apps/apk_web_app_service.h"
#include "chrome/browser/ash/apps/apk_web_app_service_factory.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/arc/session/arc_session_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/ash/components/standalone_browser/feature_refs.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/webapps/browser/install_result_code.h"
#include "components/webapps/browser/uninstall_result_code.h"
#include "content/public/test/browser_test.h"

namespace ash {

namespace {

arc::mojom::ArcPackageInfoPtr GetArcAppPackage(const std::string& name) {
  return arc::mojom::ArcPackageInfo::New("org.example." + name,
                                         /*package_version=*/1,
                                         /*last_backup_android_id=*/1,
                                         /*last_backup_time=*/1,
                                         /*sync=*/true,
                                         /*system=*/false);
}

arc::mojom::ArcPackageInfoPtr GetWebAppPackage(const std::string& name) {
  auto package = GetArcAppPackage(name);
  package->web_app_info = arc::mojom::WebAppInfo::New(
      /*title=*/name,
      /*start_url=*/"https://example.org/" + name + "?start",
      /*scope_url=*/"https://example.org/" + name,
      /*theme_color=*/100000,
      /*is_web_only_twa=*/true,
      /*certificate_sha256_fingerprint=*/name + "-sha1");
  return package;
}

}  // namespace

class ApkWebAppServiceLacrosBrowserTest : public InProcessBrowserTest,
                                          public ApkWebAppService::Delegate {
 public:
  ApkWebAppServiceLacrosBrowserTest() {
    scoped_feature_list_.InitWithFeatures(
        ash::standalone_browser::GetFeatureRefs(), {});
    scoped_command_line_.GetProcessCommandLine()->AppendSwitch(
        ash::switches::kEnableLacrosForTesting);
    dependency_manager_subscription_ =
        BrowserContextDependencyManager::GetInstance()
            ->RegisterCreateServicesCallbackForTesting(base::BindRepeating(
                &ApkWebAppServiceLacrosBrowserTest::SetTestingFactory,
                base::Unretained(this)));
  }

  void SetTestingFactory(content::BrowserContext* context) {
    ApkWebAppServiceFactory::GetInstance()->SetTestingFactory(
        context, base::BindRepeating(
                     &ApkWebAppServiceLacrosBrowserTest::CreateApkWebAppService,
                     base::Unretained(this)));
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    arc::SetArcAvailableCommandLineForTesting(command_line);
  }

  void SetUpInProcessBrowserTestFixture() override {
    arc::ArcSessionManager::SetUiEnabledForTesting(false);
  }

  void SetUpOnMainThread() override {
    // Create a new ash browser window for things that use browser().
    Profile* profile = ProfileManager::GetActiveUserProfile();
    DCHECK(profile);
    chrome::NewEmptyWindow(profile);
    SelectFirstBrowser();
    DCHECK(browser());
    DCHECK_EQ(browser()->profile(), profile);

    arc::SetArcPlayStoreEnabledForProfile(browser()->profile(), true);
  }

  // `ApkWebAppService::Delegate` implementation, stubs out ARC And Lacros.

  void MaybeInstallWebAppInLacros(const std::string& package_name,
                                  arc::mojom::WebAppInfoPtr web_app_info,
                                  WebAppInstallCallback callback) override {
    if (!lacros_running_) {
      return;
    }

    auto app = std::make_unique<apps::App>(
        apps::AppType::kWeb,
        web_app::GenerateAppId(
            /*manifest_id=*/std::nullopt, GURL(web_app_info->start_url)));
    app->readiness = apps::Readiness::kReady;
    app->publisher_id = web_app_info->start_url;

    lacros_web_apps_[app->app_id] = app->Clone();

    std::vector<apps::AppPtr> apps;
    apps.push_back(app->Clone());
    PublishToAppService(std::move(apps));

    std::move(callback).Run(app->app_id, web_app_info->is_web_only_twa,
                            web_app_info->certificate_sha256_fingerprint,
                            webapps::InstallResultCode::kSuccessNewInstall);
  }

  void MaybeUninstallWebAppInLacros(const webapps::AppId& web_app_id,
                                    WebAppUninstallCallback callback) override {
    if (!lacros_running_) {
      return;
    }

    auto it = lacros_web_apps_.find(web_app_id);

    // Do not publish the uninstall to App Service if the app is marked as
    // also installed by the browser, as the real web app provider removes the
    // ARC install source but keeps the app installed.
    if (it != lacros_web_apps_.end() &&
        !base::Contains(browser_installed_apps_, web_app_id)) {
      auto app = std::move(it->second);
      app->readiness = apps::Readiness::kUninstalledByUser;
      lacros_web_apps_.erase(it);
      std::vector<apps::AppPtr> apps;
      apps.push_back(std::move(app));
      PublishToAppService(std::move(apps));
    }

    std::move(callback).Run(webapps::UninstallResultCode::kAppRemoved);
  }

  void MaybeUninstallPackageInArc(const std::string& package_name) override {
    if (!arc_running_) {
      return;
    }

    GetAppHost().OnPackageRemoved(package_name);
  }

 protected:
  // Test ApkWebAppService factory.
  std::unique_ptr<KeyedService> CreateApkWebAppService(
      content::BrowserContext* context) {
    Profile* profile = static_cast<Profile*>(context);
    return std::make_unique<ApkWebAppService>(profile, this);
  }

  ApkWebAppService& GetApkWebAppService() {
    auto* service = ApkWebAppService::Get(browser()->profile());
    DCHECK(service);
    return *service;
  }

  apps::AppRegistryCache& GetAppRegistryCache() {
    auto* proxy =
        apps::AppServiceProxyFactory::GetForProfile(browser()->profile());
    DCHECK(proxy);
    return proxy->AppRegistryCache();
  }

  ArcAppListPrefs& GetArcAppListPrefs() {
    auto* prefs = ArcAppListPrefs::Get(browser()->profile());
    DCHECK(prefs);
    return *prefs;
  }

  arc::mojom::AppHost& GetAppHost() { return GetArcAppListPrefs(); }

  template <typename... PackageT>
  void StartArc(mojo::StructPtr<PackageT>... initial_packages) {
    // Can't use initializer list with unique_ptr since it always copies.
    arc::mojom::ArcPackageInfoPtr array[] = {std::move(initial_packages)...};
    StartArc({std::make_move_iterator(std::begin(array)),
              std::make_move_iterator(std::end(array))});
  }

  void StartArc(std::vector<arc::mojom::ArcPackageInfoPtr> initial_packages) {
    DCHECK(!arc_running_);
    arc_running_ = true;
    // Trigger a package refresh.
    arc::mojom::AppHost* app_host = ArcAppListPrefs::Get(browser()->profile());
    app_host->OnPackageListRefreshed(std::move(initial_packages));
  }

  void StopArc() {
    DCHECK(arc_running_);
    arc_running_ = false;
  }

  void StartLacros(std::vector<apps::AppPtr> initial_apps) {
    DCHECK(!lacros_running_);
    lacros_running_ = true;
    // Publish initial apps.
    PublishToAppService(std::move(initial_apps));
    crosapi::WebAppServiceAsh::Observer& observer = GetApkWebAppService();
    observer.OnWebAppProviderBridgeConnected();
  }

  void StartLacros() { StartLacros(GetLacrosWebApps()); }

  void StopLacros() {
    DCHECK(lacros_running_);
    lacros_running_ = false;
  }

  std::vector<apps::AppPtr> GetLacrosWebApps() {
    std::vector<apps::AppPtr> apps;
    for (const auto& [app_id, app] : lacros_web_apps_) {
      apps.push_back(app->Clone());
    }
    return apps;
  }

  void PublishToAppService(std::vector<apps::AppPtr> apps) {
    auto* proxy =
        apps::AppServiceProxyFactory::GetForProfile(browser()->profile());
    // Emulate what |apps::WebAppsCrosapi| does, need to publish apps for
    // |apps::AppRegistryCache| to get the update.
    proxy->OnApps(std::move(apps), apps::AppType::kWeb,
                  !published_initial_apps_);
    published_initial_apps_ = true;
  }

  // Check that the app is installed in the app registry.
  bool IsWebAppInstalled(const std::string& start_url) {
    bool in_app_service = false;
    GetAppRegistryCache().ForEachApp([&](const apps::AppUpdate& update) {
      if (update.PublisherId() == start_url &&
          update.Readiness() == apps::Readiness::kReady) {
        in_app_service = true;
      }
    });
    return in_app_service;
  }

  void SetAppInstalledInBrowser(const std::string& app_id) {
    browser_installed_apps_.emplace(app_id);
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
  base::test::ScopedCommandLine scoped_command_line_;
  base::CallbackListSubscription dependency_manager_subscription_;

  bool arc_running_ = false;
  bool lacros_running_ = false;
  bool published_initial_apps_ = false;

  std::map<std::string, apps::AppPtr> lacros_web_apps_;
  std::set<std::string> browser_installed_apps_;
};

IN_PROC_BROWSER_TEST_F(ApkWebAppServiceLacrosBrowserTest, InstallAndUninstall) {
  auto& service = GetApkWebAppService();

  // Start with one web app and one Android app in ARC.
  StartLacros();
  StartArc(GetWebAppPackage("a"), GetArcAppPackage("b"));

  // App "a" installed.
  std::optional<std::string> app_id_a =
      service.GetWebAppIdForPackageName("org.example.a");
  ASSERT_NE(app_id_a, std::nullopt);
  EXPECT_TRUE(service.IsWebOnlyTwa(*app_id_a));
  EXPECT_EQ(service.GetCertificateSha256Fingerprint(*app_id_a), "a-sha1");
  EXPECT_TRUE(IsWebAppInstalled("https://example.org/a?start"));
  // App "b" not installed.
  ASSERT_EQ(service.GetWebAppIdForPackageName("org.example.b"), std::nullopt);

  // Incrementally install a web app in ARC.
  GetAppHost().OnPackageAdded(GetWebAppPackage("c"));

  // App "c" installed.
  std::optional<std::string> app_id_c =
      service.GetWebAppIdForPackageName("org.example.c");
  ASSERT_NE(app_id_c, std::nullopt);
  EXPECT_TRUE(service.IsWebOnlyTwa(*app_id_c));
  EXPECT_EQ(service.GetCertificateSha256Fingerprint(*app_id_c), "c-sha1");
  EXPECT_TRUE(IsWebAppInstalled("https://example.org/c?start"));

  // Incrementally uninstall a web app in ARC.
  GetAppHost().OnPackageRemoved("org.example.a");

  // App "a" uninstalled.
  ASSERT_EQ(service.GetWebAppIdForPackageName("org.example.a"), std::nullopt);
  EXPECT_FALSE(IsWebAppInstalled("https://example.org/a?start"));

  // Uninstall an app by removing it from the initial refresh list.
  StopArc();
  StartArc({});

  // App "c" uninstalled.
  ASSERT_EQ(service.GetWebAppIdForPackageName("org.example.c"), std::nullopt);
  EXPECT_FALSE(IsWebAppInstalled("https://example.org/c?start"));
}

IN_PROC_BROWSER_TEST_F(ApkWebAppServiceLacrosBrowserTest, UpdateAppType) {
  auto& service = GetApkWebAppService();
  auto* shelf_model = ShelfModel::Get();

  // Start with one web app in ARC.
  StartLacros();
  StartArc(GetWebAppPackage("a"));

  // App "a" is installed.
  std::optional<std::string> app_id_a =
      service.GetWebAppIdForPackageName("org.example.a");
  ASSERT_NE(app_id_a, std::nullopt);
  EXPECT_TRUE(IsWebAppInstalled("https://example.org/a?start"));

  // Pin the app to the shelf.
  PinAppWithIDToShelf(*app_id_a);
  EXPECT_TRUE(shelf_model->IsAppPinned(*app_id_a));
  int pin_index = shelf_model->ItemIndexByID(ShelfID(*app_id_a));

  // Replace with Android app.
  std::vector<arc::mojom::AppInfoPtr> apps;
  apps.push_back(arc::mojom::AppInfo::New("Title", "org.example.a",
                                          "org.example.a.activity",
                                          /*sticky=*/true));
  GetAppHost().OnPackageAppListRefreshed("org.example.a", std::move(apps));
  GetAppHost().OnPackageAdded(GetArcAppPackage("a"));

  // App "a" is uninstalled.
  ASSERT_EQ(service.GetWebAppIdForPackageName("org.example.a"), std::nullopt);
  EXPECT_FALSE(IsWebAppInstalled("https://example.org/a?start"));
  // Android app is still pinned.
  auto arc_app_id = GetArcAppListPrefs().GetAppIdByPackageName("org.example.a");
  EXPECT_TRUE(shelf_model->IsAppPinned(arc_app_id));
  EXPECT_EQ(shelf_model->ItemIndexByID(ShelfID(arc_app_id)), pin_index);

  // Move pin to the left, and then reinstall the web app.
  ASSERT_TRUE(shelf_model->Swap(pin_index, /*with_next=*/false));
  pin_index--;
  GetAppHost().OnPackageAdded(GetWebAppPackage("a"));

  // App "a" is installed and has the updated pin index.
  app_id_a = service.GetWebAppIdForPackageName("org.example.a");
  ASSERT_NE(app_id_a, std::nullopt);
  EXPECT_TRUE(IsWebAppInstalled("https://example.org/a?start"));
  EXPECT_TRUE(shelf_model->IsAppPinned(*app_id_a));
  EXPECT_EQ(shelf_model->ItemIndexByID(ShelfID(*app_id_a)), pin_index);
}

IN_PROC_BROWSER_TEST_F(ApkWebAppServiceLacrosBrowserTest,
                       DelayedLacrosInstallUninstall) {
  auto& service = GetApkWebAppService();

  // Start ARC only with one web app.
  StartArc(GetWebAppPackage("a"));

  // App "a" won't be installed because Lacros isn't running.
  EXPECT_FALSE(IsWebAppInstalled("https://example.org/a?start"));
  ASSERT_EQ(service.GetWebAppIdForPackageName("org.example.a"), std::nullopt);

  // Start Lacros, app "a" should now be installed.
  StartLacros();
  EXPECT_TRUE(IsWebAppInstalled("https://example.org/a?start"));
  ASSERT_NE(service.GetWebAppIdForPackageName("org.example.a"), std::nullopt);

  // Stop Lacros and install another app incrementally.
  StopLacros();
  GetAppHost().OnPackageAdded(GetWebAppPackage("b"));

  // App "b" won't be installed because Lacros isn't running.
  EXPECT_FALSE(IsWebAppInstalled("https://example.org/b?start"));
  ASSERT_EQ(service.GetWebAppIdForPackageName("org.example.b"), std::nullopt);

  // Start Lacros, app "b" should now be installed.
  StartLacros();
  EXPECT_TRUE(IsWebAppInstalled("https://example.org/b?start"));
  ASSERT_NE(service.GetWebAppIdForPackageName("org.example.b"), std::nullopt);

  // Stop Lacros and uninstall app "a".
  StopLacros();
  GetAppHost().OnPackageRemoved("org.example.a");

  // App "a" should still be installed because Lacros isn't running.
  EXPECT_TRUE(IsWebAppInstalled("https://example.org/a?start"));
  ASSERT_NE(service.GetWebAppIdForPackageName("org.example.a"), std::nullopt);

  // Start Lacros again, app "a" should now be removed.
  StartLacros();
  EXPECT_FALSE(IsWebAppInstalled("https://example.org/a?start"));
  ASSERT_EQ(service.GetWebAppIdForPackageName("org.example.a"), std::nullopt);
}

IN_PROC_BROWSER_TEST_F(ApkWebAppServiceLacrosBrowserTest,
                       UninstallWebAppThenStartArc) {
  auto& service = GetApkWebAppService();

  // Start with one web app in ARC.
  StartLacros();
  StartArc(GetWebAppPackage("a"));

  // App "a" is installed.
  std::optional<std::string> app_id_a =
      service.GetWebAppIdForPackageName("org.example.a");
  ASSERT_NE(app_id_a, std::nullopt);
  EXPECT_TRUE(IsWebAppInstalled("https://example.org/a?start"));

  // Stop ARC and uninstall app "a" from the browser side.
  StopArc();
  MaybeUninstallWebAppInLacros(*app_id_a, base::DoNothing());

  // Prefs should still be there, but the web app is uninstalled.
  EXPECT_NE(GetArcAppListPrefs().GetPackage("org.example.a"), nullptr);
  ASSERT_NE(service.GetWebAppIdForPackageName("org.example.a"), std::nullopt);
  EXPECT_FALSE(IsWebAppInstalled("https://example.org/a?start"));

  // Restart ARC with the same packages, will trigger ARC app uninstallation.
  StartArc(GetWebAppPackage("a"));
  EXPECT_EQ(GetArcAppListPrefs().GetPackage("org.example.a"), nullptr);
  EXPECT_EQ(service.GetWebAppIdForPackageName("org.example.a"), std::nullopt);
}

IN_PROC_BROWSER_TEST_F(ApkWebAppServiceLacrosBrowserTest,
                       RemoveWebAppWhenArcDisabled) {
  auto& service = GetApkWebAppService();

  StartLacros();
  StartArc(GetWebAppPackage("a"));

  // App "a" is installed.
  std::optional<std::string> app_id_a =
      service.GetWebAppIdForPackageName("org.example.a");
  ASSERT_NE(app_id_a, std::nullopt);
  EXPECT_TRUE(IsWebAppInstalled("https://example.org/a?start"));

  // Disable ARC through settings.
  base::test::TestFuture<const std::string&, const webapps::AppId&>
      uninstalled_future;
  service.SetWebAppUninstalledCallbackForTesting(
      uninstalled_future.GetCallback());
  arc::SetArcPlayStoreEnabledForProfile(browser()->profile(), false);
  StopArc();

  ASSERT_TRUE(uninstalled_future.Wait());

  // Web app should be uninstalled.
  EXPECT_EQ(service.GetWebAppIdForPackageName("org.example.a"), std::nullopt);
  EXPECT_FALSE(IsWebAppInstalled("https://example.org/a?start"));
}

IN_PROC_BROWSER_TEST_F(ApkWebAppServiceLacrosBrowserTest,
                       InstallAndUninstallArcOverUserInstall) {
  auto& service = GetApkWebAppService();

  StartLacros();
  StartArc(GetWebAppPackage("a"));

  // App "a" should be installed.
  std::optional<std::string> app_id_a =
      service.GetWebAppIdForPackageName("org.example.a");
  EXPECT_TRUE(service.IsWebOnlyTwa(*app_id_a));
  EXPECT_TRUE(IsWebAppInstalled("https://example.org/a?start"));

  // Mark the app as also installed by the browser, so that uninstalling the ARC
  // package doesn't remove the app.
  SetAppInstalledInBrowser(*app_id_a);

  // Uninstall the web app from ARC.
  GetAppHost().OnPackageRemoved("org.example.a");

  // Web app should be removed from ApkWebAppService, but is still installed.
  EXPECT_EQ(service.GetWebAppIdForPackageName("org.example.a"), std::nullopt);
  EXPECT_TRUE(IsWebAppInstalled("https://example.org/a?start"));
}

}  // namespace ash