chromium/chrome/browser/ui/ash/shelf/app_service/app_service_app_window_browsertest.cc

// 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 <memory>
#include <vector>

#include "ash/components/arc/session/arc_bridge_service.h"
#include "ash/components/arc/session/arc_service_manager.h"
#include "ash/components/arc/test/arc_util_test_support.h"
#include "ash/components/arc/test/fake_app_instance.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/ranges/algorithm.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.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/apps/platform_apps/app_browsertest_util.h"
#include "chrome/browser/ash/app_list/arc/arc_app_list_prefs.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/arc/session/arc_session_manager.h"
#include "chrome/browser/ash/borealis/borealis_service.h"
#include "chrome/browser/ash/borealis/borealis_window_manager.h"
#include "chrome/browser/ash/borealis/borealis_window_manager_mock.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service_factory.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller_test_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/test/with_crosapi_param.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/test/base/chromeos/ash_browser_test_starter.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/ash/components/standalone_browser/feature_refs.h"
#include "components/app_constants/constants.h"
#include "components/exo/shell_surface_util.h"
#include "components/services/app_service/public/cpp/app_instance_waiter.h"
#include "components/services/app_service/public/cpp/instance.h"
#include "components/services/app_service/public/cpp/instance_registry.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_navigation_observer.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/common/extension.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/display/display.h"
#include "ui/views/widget/widget.h"

using web_app::test::CrosapiParam;
using web_app::test::WithCrosapiParam;

namespace mojo {

template <>
struct TypeConverter<arc::mojom::ArcPackageInfoPtr,
                     arc::mojom::ArcPackageInfo> {
  static arc::mojom::ArcPackageInfoPtr Convert(
      const arc::mojom::ArcPackageInfo& package_info) {
    return package_info.Clone();
  }
};

}  // namespace mojo

namespace {

constexpr char kTestAppName[] = "Test ARC App";
constexpr char kTestAppName2[] = "Test ARC App 2";
constexpr char kTestPaymentAppName[] = "Test ARC Payment App";
constexpr char kTestAppPackage[] = "test.arc.app.package";
constexpr char kTestAppActivity[] = "test.arc.app.package.activity";
constexpr char kTestAppActivity2[] = "test.arc.gitapp.package.activity2";
constexpr char kTestPaymentAppPackage[] = "org.chromium.arc.payment_app";
constexpr char kTestPaymentAppActivity[] =
    "org.chromium.arc.payment_app.InvokePaymentAppActivity";

ash::ShelfAction SelectItem(
    const ash::ShelfID& id,
    ui::EventType event_type = ui::EventType::kMousePressed,
    int64_t display_id = display::kInvalidDisplayId,
    ash::ShelfLaunchSource source = ash::LAUNCH_FROM_UNKNOWN) {
  return SelectShelfItem(id, event_type, display_id, source);
}

std::string GetTestApp1Id(const std::string& package_name) {
  return ArcAppListPrefs::GetAppId(package_name, kTestAppActivity);
}

std::string GetTestApp2Id(const std::string& package_name) {
  return ArcAppListPrefs::GetAppId(package_name, kTestAppActivity2);
}

std::string GetTestPaymentAppId(const std::string& package_name) {
  return ArcAppListPrefs::GetAppId(package_name, kTestPaymentAppActivity);
}

std::vector<arc::mojom::AppInfoPtr> GetTestAppsList(
    const std::string& package_name,
    bool multi_app,
    bool payment_app) {
  std::vector<arc::mojom::AppInfoPtr> apps;

  arc::mojom::AppInfoPtr app(arc::mojom::AppInfo::New());
  app->name = kTestAppName;
  app->package_name = package_name;
  app->activity = kTestAppActivity;
  app->sticky = false;
  apps.push_back(std::move(app));

  if (multi_app) {
    app = arc::mojom::AppInfo::New();
    app->name = kTestAppName2;
    app->package_name = package_name;
    app->activity = kTestAppActivity2;
    app->sticky = false;
    apps.push_back(std::move(app));
  }

  if (payment_app) {
    app = arc::mojom::AppInfo::New();
    app->name = kTestPaymentAppName;
    app->package_name = package_name;
    app->activity = kTestPaymentAppActivity;
    app->sticky = false;
    apps.push_back(std::move(app));
  }

  return apps;
}

std::string CreateIntentUriWithShelfGroupAndLogicalWindow(
    const std::string& shelf_group_id,
    const std::string& logical_window_id) {
  return base::StringPrintf(
      "#Intent;S.org.chromium.arc.logical_window_id=%s;"
      "S.org.chromium.arc.shelf_group_id=%s;end",
      logical_window_id.c_str(), shelf_group_id.c_str());
}

// Creates an exo app window and sets its shell application id. The returned
// Widget is owned by its NativeWidget (the underlying aura::Window).
views::Widget* CreateExoWindow(const std::string& window_app_id) {
  views::Widget::InitParams params(
      views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW);
  params.bounds = gfx::Rect(5, 5, 20, 20);
  params.context = ash::Shell::GetPrimaryRootWindow();
  views::Widget* widget = new views::Widget();
  widget->Init(std::move(params));
  widget->GetNativeWindow()->SetTitle(u"foo");
  // Set app id before showing the window to be recognized in
  // AppServiceAppWindowShelfController.
  exo::SetShellApplicationId(widget->GetNativeWindow(), window_app_id);
  widget->Show();
  widget->Activate();
  return widget;
}

}  // namespace

class AppServiceAppWindowBrowserTest
    : public extensions::PlatformAppBrowserTest {
 protected:
  AppServiceAppWindowBrowserTest() = default;

  ~AppServiceAppWindowBrowserTest() override = default;

  void SetUp() override { extensions::PlatformAppBrowserTest::SetUp(); }

  void SetUpOnMainThread() override {
    controller_ = ChromeShelfController::instance();
    ASSERT_TRUE(controller_);
    extensions::PlatformAppBrowserTest::SetUpOnMainThread();

    app_service_proxy_ = apps::AppServiceProxyFactory::GetForProfile(profile());
    ASSERT_TRUE(app_service_proxy_);
  }

  ash::ShelfModel* shelf_model() { return controller_->shelf_model(); }

  // Returns the last item in the shelf.
  const ash::ShelfItem& GetLastShelfItem() {
    return shelf_model()->items()[shelf_model()->item_count() - 1];
  }

  apps::InstanceState GetAppInstanceState(const std::string& app_id,
                                          const aura::Window* window) {
    std::set<apps::InstanceState> states;
    app_service_proxy_->InstanceRegistry().ForInstancesWithWindow(
        window, [&](const apps::InstanceUpdate& update) {
          if (update.AppId() == app_id) {
            states.insert(update.State());
          }
        });
    if (states.size() == 1)
      return *states.begin();
    return apps::InstanceState::kUnknown;
  }

  raw_ptr<ChromeShelfController, DanglingUntriaged> controller_ = nullptr;
  raw_ptr<apps::AppServiceProxy, DanglingUntriaged> app_service_proxy_ =
      nullptr;
};

// Test that we have the correct instance for Chrome apps.
IN_PROC_BROWSER_TEST_F(AppServiceAppWindowBrowserTest, ExtensionAppsWindow) {
  const extensions::Extension* app =
      LoadAndLaunchPlatformApp("launch", "Launched");
  extensions::AppWindow* app_window = CreateAppWindow(profile(), app);
  ASSERT_TRUE(app_window);

  auto instances =
      app_service_proxy_->InstanceRegistry().GetInstances(app->id());
  EXPECT_EQ(1u, instances.size());
  auto* window = (*instances.begin())->Window();

  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            GetAppInstanceState(app->id(), window));

  const ash::ShelfItem& item = GetLastShelfItem();
  // Since it is already active, clicking it should minimize.
  SelectItem(item.id);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning,
            GetAppInstanceState(app->id(), window));

  // Click the item again to activate the app.
  SelectItem(item.id);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            GetAppInstanceState(app->id(), window));

  CloseAppWindow(app_window);
  instances = app_service_proxy_->InstanceRegistry().GetInstances(app->id());
  EXPECT_TRUE(instances.empty());
}

// Test that we have the correct instances with more than one window.
IN_PROC_BROWSER_TEST_F(AppServiceAppWindowBrowserTest, MultipleWindows) {
  const extensions::Extension* app =
      LoadAndLaunchPlatformApp("launch", "Launched");
  extensions::AppWindow* app_window1 = CreateAppWindow(profile(), app);

  auto instances =
      app_service_proxy_->InstanceRegistry().GetInstances(app->id());
  auto* window1 = (*instances.begin())->Window();

  // Add a second window; confirm the shelf item stays; check the app menu.
  extensions::AppWindow* app_window2 = CreateAppWindow(profile(), app);

  instances = app_service_proxy_->InstanceRegistry().GetInstances(app->id());
  EXPECT_EQ(2u, instances.size());
  aura::Window* window2 = nullptr;
  for (const apps::Instance* instance : instances) {
    if (instance->Window() != window1) {
      window2 = instance->Window();
    }
  }

  // The window1 is inactive.
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kVisible,
            GetAppInstanceState(app->id(), window1));

  // The window2 is active.
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            GetAppInstanceState(app->id(), window2));

  // Close the second window; confirm the shelf item stays; check the app menu.
  CloseAppWindow(app_window2);
  instances = app_service_proxy_->InstanceRegistry().GetInstances(app->id());
  EXPECT_EQ(1u, instances.size());

  // The window1 is active again.
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            GetAppInstanceState(app->id(), window1));

  // Close the first window; the shelf item should be removed.
  CloseAppWindow(app_window1);
  instances = app_service_proxy_->InstanceRegistry().GetInstances(app->id());
  EXPECT_TRUE(instances.empty());
}

// Test that we have the correct instances with one HostedApp and one window.
IN_PROC_BROWSER_TEST_F(AppServiceAppWindowBrowserTest,
                       HostedAppandExtensionApp) {
  const extensions::Extension* extension1 = InstallHostedApp();
  LaunchHostedApp(extension1);

  std::string app_id1 = extension1->id();
  auto instances = app_service_proxy_->InstanceRegistry().GetInstances(app_id1);
  EXPECT_EQ(1u, instances.size());
  auto* instance1 = (*instances.begin()).get();
  EXPECT_NE(instance1->Window(), instance1->Window()->GetToplevelWindow());

  // The window1 is active.
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kVisible | apps::InstanceState::kActive,
            GetAppInstanceState(app_id1, instance1->Window()));

  // Add an Extension app.
  const extensions::Extension* extension2 =
      LoadAndLaunchPlatformApp("launch", "Launched");
  auto* app_window = CreateAppWindow(profile(), extension2);

  std::string app_id2 = extension2->id();
  instances = app_service_proxy_->InstanceRegistry().GetInstances(app_id2);
  EXPECT_EQ(1u, instances.size());
  auto* instance2 = (*instances.begin()).get();
  EXPECT_EQ(instance2->Window(), instance2->Window()->GetToplevelWindow());

  // The window1 is inactive.
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kVisible,
            GetAppInstanceState(app_id1, instance1->Window()));

  // The window2 is active.
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kVisible | apps::InstanceState::kActive,
            GetAppInstanceState(app_id2, instance2->Window()));

  // Close the Extension app's window..
  CloseAppWindow(app_window);
  instances = app_service_proxy_->InstanceRegistry().GetInstances(app_id2);
  EXPECT_TRUE(instances.empty());

  // The window1 is active.
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kVisible | apps::InstanceState::kActive,
            GetAppInstanceState(app_id1, instance1->Window()));

  // Close the HostedApp.
  TabStripModel* tab_strip = browser()->tab_strip_model();
  tab_strip->CloseWebContentsAt(tab_strip->active_index(),
                                TabCloseTypes::CLOSE_NONE);

  instances = app_service_proxy_->InstanceRegistry().GetInstances(app_id1);
  EXPECT_TRUE(instances.empty());
}

IN_PROC_BROWSER_TEST_F(AppServiceAppWindowBrowserTest, AshBrowserWindow) {
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("chrome://blank")));

  auto instances = app_service_proxy_->InstanceRegistry().GetInstances(
      app_constants::kChromeAppId);
  EXPECT_EQ(1u, instances.size());
  auto* instance = (*instances.begin()).get();
  EXPECT_EQ(instance->Window(), instance->Window()->GetToplevelWindow());
}

class AppServiceAppWindowLacrosBrowserTest
    : public AppServiceAppWindowBrowserTest {
 public:
  AppServiceAppWindowLacrosBrowserTest() = default;
  ~AppServiceAppWindowLacrosBrowserTest() override = default;

  void SetUpInProcessBrowserTestFixture() override {
    AppServiceAppWindowBrowserTest::SetUpInProcessBrowserTestFixture();
    if (lacros_starter_.HasLacrosArgument()) {
      ASSERT_TRUE(lacros_starter_.PrepareEnvironmentForLacros());
    }
  }

  void SetUpOnMainThread() override {
    AppServiceAppWindowBrowserTest::SetUpOnMainThread();
    if (!lacros_starter_.HasLacrosArgument()) {
      GTEST_SKIP() << "This test needs to run together with Lacros but the "
                      "--lacros-chrome-path switch is missing.";
    }
    lacros_starter_.StartLacros(this);
  }

  void WaitForLacrosInstanceState(apps::InstanceState state) {
    apps::AppInstanceWaiter(app_service_proxy_->InstanceRegistry(),
                            app_constants::kLacrosAppId, state)
        .Await();
  }

  std::set<raw_ptr<const apps::Instance, SetExperimental>>
  GetLacrosInstances() {
    return app_service_proxy_->InstanceRegistry().GetInstances(
        app_constants::kLacrosAppId);
  }

  void SelectLacrosShelfItem() {
    int lacros_index =
        shelf_model()->ItemIndexByAppID(app_constants::kLacrosAppId);
    ASSERT_NE(-1, lacros_index);
    const ash::ShelfItem& item = shelf_model()->items()[lacros_index];
    SelectItem(item.id);
  }

 protected:
  test::AshBrowserTestStarter lacros_starter_;
};

IN_PROC_BROWSER_TEST_F(AppServiceAppWindowLacrosBrowserTest, LacrosWindow) {
  // Wait for the initial Lacros window.
  WaitForLacrosInstanceState(apps::InstanceState(
      apps::kStarted | apps::kRunning | apps::kActive | apps::kVisible));
  {
    auto instances = GetLacrosInstances();
    ASSERT_EQ(1u, instances.size());
    auto* instance = (*instances.begin()).get();
    EXPECT_EQ(instance->Window(), instance->Window()->GetToplevelWindow());
  }

  // Since it is already active, clicking it should minimize.
  SelectLacrosShelfItem();
  WaitForLacrosInstanceState(
      apps::InstanceState(apps::kStarted | apps::kRunning));

  // Click the item again to activate the window.
  SelectLacrosShelfItem();
  WaitForLacrosInstanceState(apps::InstanceState(
      apps::kStarted | apps::kRunning | apps::kActive | apps::kVisible));

  views::Widget::GetWidgetForNativeView(lacros_starter_.initial_lacros_window())
      ->CloseNow();
  EXPECT_TRUE(GetLacrosInstances().empty());
}

class AppServiceAppWindowBorealisBrowserTest
    : public AppServiceAppWindowBrowserTest {
 public:
  ~AppServiceAppWindowBorealisBrowserTest() override = default;

  std::string MakeBorealisApp(const std::string& vm,
                              const std::string& container,
                              const std::string& name) {
    vm_tools::apps::ApplicationList list;
    list.set_vm_name(vm);
    list.set_container_name(container);
    list.set_vm_type(vm_tools::apps::BOREALIS);
    vm_tools::apps::App* app = list.add_apps();
    app->set_desktop_file_id(name);
    app->mutable_name()->add_values()->set_value(name);
    app->set_no_display(false);
    guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile())
        ->UpdateApplicationList(list);

    return guest_os::GuestOsRegistryService::GenerateAppId(name, vm, container);
  }
};

IN_PROC_BROWSER_TEST_F(AppServiceAppWindowBorealisBrowserTest,
                       BorealisKnownApp) {
  // Generate a fake app.
  std::string app_id = MakeBorealisApp("vm", "container", "foo");

  views::Widget* widget =
      CreateExoWindow("org.chromium.guest_os.borealis.wmclass.foo");

  EXPECT_EQ(1u,
            app_service_proxy_->InstanceRegistry().GetInstances(app_id).size());
  EXPECT_NE(-1, shelf_model()->ItemIndexByAppID(app_id));

  widget->CloseNow();
  EXPECT_TRUE(
      app_service_proxy_->InstanceRegistry().GetInstances(app_id).empty());
}

IN_PROC_BROWSER_TEST_F(AppServiceAppWindowBorealisBrowserTest,
                       BorealisUnknownApp) {
  views::Widget* widget =
      CreateExoWindow("org.chromium.guest_os.borealis.wmclass.bar");
  std::string app_id =
      "borealis_anon:org.chromium.guest_os.borealis.wmclass.bar";

  EXPECT_EQ(1u,
            app_service_proxy_->InstanceRegistry().GetInstances(app_id).size());
  ASSERT_NE(-1, shelf_model()->ItemIndexByAppID(app_id));

  // Now that the app is published, it will have a name based on the window title
  EXPECT_EQ(
      "foo",
      base::UTF16ToUTF8(shelf_model()
                            ->items()[shelf_model()->ItemIndexByAppID(app_id)]
                            .title));

  widget->CloseNow();
  EXPECT_TRUE(
      app_service_proxy_->InstanceRegistry().GetInstances(app_id).empty());
}

IN_PROC_BROWSER_TEST_F(AppServiceAppWindowBorealisBrowserTest,
                       BorealisSession) {
  std::string app_id = MakeBorealisApp("vm", "container", "foo");

  testing::StrictMock<borealis::MockLifetimeObserver> observer;
  base::ScopedObservation<
      borealis::BorealisWindowManager,
      borealis::BorealisWindowManager::AppWindowLifetimeObserver>
      observation(&observer);
  observation.Observe(
      &borealis::BorealisService::GetForProfile(profile())->WindowManager());

  testing::InSequence sequence;
  EXPECT_CALL(observer, OnSessionStarted());
  EXPECT_CALL(observer, OnAppStarted(app_id));
  EXPECT_CALL(observer, OnWindowStarted(app_id, testing::_));
  views::Widget* widget =
      CreateExoWindow("org.chromium.guest_os.borealis.wmclass.foo");

  EXPECT_CALL(observer, OnWindowFinished(app_id, widget->GetNativeWindow()));
  EXPECT_CALL(observer, OnAppFinished(app_id, widget->GetNativeWindow()));
  EXPECT_CALL(observer, OnSessionFinished());
  widget->CloseNow();
}

class AppServiceAppWindowWebAppBrowserTest
    : public AppServiceAppWindowBrowserTest {
 protected:
  AppServiceAppWindowWebAppBrowserTest() = default;
  ~AppServiceAppWindowWebAppBrowserTest() override = default;

  // AppServiceAppWindowBrowserTest:
  void SetUpOnMainThread() override {
    AppServiceAppWindowBrowserTest::SetUpOnMainThread();

    https_server_.AddDefaultHandlers(GetChromeTestDataDir());
    ASSERT_TRUE(https_server_.Start());
  }

  // |SetUpWebApp()| must be called after |SetUpOnMainThread()| to make sure
  // the Network Service process has been setup properly.
  std::string CreateWebApp() const {
    auto web_app_info =
        web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(GetAppURL());
    web_app_info->scope = GetAppURL().GetWithoutFilename();

    std::string app_id = web_app::test::InstallWebApp(browser()->profile(),
                                                      std::move(web_app_info));
    CreateWebAppWindow(app_id);
    return app_id;
  }

  GURL GetAppURL() const {
    return https_server_.GetURL("app.com", "/ssl/google.html");
  }

  void CreateWebAppWindow(const std::string& app_id) const {
    content::TestNavigationObserver navigation_observer(GetAppURL());
    navigation_observer.StartWatchingNewWebContents();
    web_app::LaunchWebAppBrowser(browser()->profile(), app_id);
    navigation_observer.WaitForNavigationFinished();
  }

 private:
  // For mocking a secure site.
  net::EmbeddedTestServer https_server_;
};

// Test that we have the correct instance for Web apps.
IN_PROC_BROWSER_TEST_F(AppServiceAppWindowWebAppBrowserTest, WebAppsWindow) {
  std::string app_id = CreateWebApp();

  auto instances = app_service_proxy_->InstanceRegistry().GetInstances(app_id);
  EXPECT_EQ(1u, instances.size());
  EXPECT_NE((*instances.begin())->Window(),
            (*instances.begin())->Window()->GetToplevelWindow());
  const auto* instance = (*instances.begin()).get();
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            GetAppInstanceState(app_id, instance->Window()));

  const ash::ShelfItem& item = GetLastShelfItem();
  // Since it is already active, clicking it should minimize.
  SelectItem(item.id);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning,
            GetAppInstanceState(app_id, instance->Window()));

  // Click the item again to activate the app.
  SelectItem(item.id);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            GetAppInstanceState(app_id, instance->Window()));

  controller_->Close(item.id);
  // Make sure that the window is closed.
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(
      app_service_proxy_->InstanceRegistry().GetInstances(app_id).empty());
}

// Tests that web app with multiple open windows can be activated from the app
// list.
IN_PROC_BROWSER_TEST_F(AppServiceAppWindowWebAppBrowserTest,
                       LaunchFromAppList) {
  std::string app_id = CreateWebApp();

  auto instances = app_service_proxy_->InstanceRegistry().GetInstances(app_id);
  ASSERT_EQ(1u, instances.size());
  const auto* instance1 = (*instances.begin()).get();
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            GetAppInstanceState(app_id, instance1->Window()));

  const ash::ShelfItem item = GetLastShelfItem();
  // Since it is already active, clicking it should minimize.
  SelectItem(item.id);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning,
            GetAppInstanceState(app_id, instance1->Window()));

  // Create another window.
  CreateWebAppWindow(app_id);
  instances = app_service_proxy_->InstanceRegistry().GetInstances(app_id);
  ASSERT_EQ(2u, instances.size());
  const auto* instance2 = *instances.begin() == instance1
                              ? (*instances.rbegin()).get()
                              : (*instances.begin()).get();

  ASSERT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            GetAppInstanceState(app_id, instance2->Window()));

  // Bring the browser window to foreground.
  browser()->window()->Show();

  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kVisible,
            GetAppInstanceState(app_id, instance2->Window()));

  // Launching the first app from the app list should activate it.
  SelectItem(item.id, ui::EventType::kMousePressed, display::kInvalidDisplayId,
             ash::LAUNCH_FROM_APP_LIST);

  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            GetAppInstanceState(app_id, instance1->Window()));

  // Selecting an active app from the app list should not minimize it.
  SelectItem(item.id, ui::EventType::kMousePressed, display::kInvalidDisplayId,
             ash::LAUNCH_FROM_APP_LIST);

  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            GetAppInstanceState(app_id, instance1->Window()));
}

class AppServiceAppWindowArcAppBrowserTest
    : public AppServiceAppWindowBrowserTest {
 protected:
  // AppServiceAppWindowBrowserTest:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    AppServiceAppWindowBrowserTest::SetUpCommandLine(command_line);
    arc::SetArcAvailableCommandLineForTesting(command_line);
  }

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

  void SetUpOnMainThread() override {
    AppServiceAppWindowBrowserTest::SetUpOnMainThread();
    arc::SetArcPlayStoreEnabledForProfile(profile(), true);

    // This ensures app_prefs()->GetApp() below never returns nullptr.
    base::RunLoop run_loop;
    app_prefs()->SetDefaultAppsReadyCallback(run_loop.QuitClosure());
    run_loop.Run();
  }

  void InstallTestApps(const std::string& package_name,
                       bool multi_app,
                       bool payment_app) {
    app_host()->OnAppListRefreshed(
        GetTestAppsList(package_name, multi_app, payment_app));

    std::unique_ptr<ArcAppListPrefs::AppInfo> app_info =
        app_prefs()->GetApp(GetTestApp1Id(package_name));
    ASSERT_TRUE(app_info);
    EXPECT_TRUE(app_info->ready);
    if (multi_app) {
      std::unique_ptr<ArcAppListPrefs::AppInfo> app_info2 =
          app_prefs()->GetApp(GetTestApp2Id(package_name));
      ASSERT_TRUE(app_info2);
      EXPECT_TRUE(app_info2->ready);
    }
    if (payment_app) {
      std::unique_ptr<ArcAppListPrefs::AppInfo> payment_app_info =
          app_prefs()->GetApp(GetTestPaymentAppId(package_name));
      ASSERT_TRUE(payment_app_info);
      EXPECT_TRUE(payment_app_info->ready);
    }
  }

  void SendPackageAdded(const std::string& package_name, bool package_synced) {
    arc::mojom::ArcPackageInfo package_info;
    package_info.package_name = package_name;
    package_info.package_version = 1;
    package_info.last_backup_android_id = 1;
    package_info.last_backup_time = 1;
    package_info.sync = package_synced;
    app_host()->OnPackageAdded(arc::mojom::ArcPackageInfo::From(package_info));

    base::RunLoop().RunUntilIdle();
  }

  void StartInstance() {
    app_instance_ = std::make_unique<arc::FakeAppInstance>(app_host());
    arc_brige_service()->app()->SetInstance(app_instance_.get());
  }

  void StopInstance() {
    if (app_instance_)
      arc_brige_service()->app()->CloseInstance(app_instance_.get());
    arc_session_manager()->Shutdown();
  }

  ArcAppListPrefs* app_prefs() { return ArcAppListPrefs::Get(profile()); }

  // Returns as AppHost interface in order to access to private implementation
  // of the interface.
  arc::mojom::AppHost* app_host() { return app_prefs(); }

 private:
  arc::ArcSessionManager* arc_session_manager() {
    return arc::ArcSessionManager::Get();
  }

  arc::ArcBridgeService* arc_brige_service() {
    return arc::ArcServiceManager::Get()->arc_bridge_service();
  }

  std::unique_ptr<arc::FakeAppInstance> app_instance_;
};

// Test that we have the correct instance for ARC apps.
IN_PROC_BROWSER_TEST_F(AppServiceAppWindowArcAppBrowserTest, ArcAppsWindow) {
  // Install app to remember existing apps.
  StartInstance();
  InstallTestApps(kTestAppPackage, true, false);
  SendPackageAdded(kTestAppPackage, false);

  // Create the window for app1.
  views::Widget* arc_window1 = CreateExoWindow("org.chromium.arc.1");
  const std::string app_id1 = GetTestApp1Id(kTestAppPackage);

  // Simulate task creation so the app is marked as running/open.
  std::unique_ptr<ArcAppListPrefs::AppInfo> info = app_prefs()->GetApp(app_id1);
  app_host()->OnTaskCreated(1, info->package_name, info->activity, info->name,
                            info->intent_uri, /*session_id=*/0);
  EXPECT_TRUE(controller_->GetItem(ash::ShelfID(app_id1)));

  // Check the window state in instance for app1
  auto instances = app_service_proxy_->InstanceRegistry().GetInstances(app_id1);
  EXPECT_EQ(1u, instances.size());
  aura::Window* window1 = (*instances.begin())->Window();
  apps::InstanceState latest_state =
      app_service_proxy_->InstanceRegistry().GetState(window1);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning,
            latest_state);

  app_host()->OnTaskSetActive(1);
  latest_state = app_service_proxy_->InstanceRegistry().GetState(window1);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            latest_state);
  controller_->shelf_model()->PinExistingItemWithID(app_id1);

  // Create the task id for app2 first, then create the window.
  const std::string app_id2 = GetTestApp2Id(kTestAppPackage);
  info = app_prefs()->GetApp(app_id2);
  app_host()->OnTaskCreated(2, info->package_name, info->activity, info->name,
                            info->intent_uri, /*session_id=*/0);
  views::Widget* arc_window2 = CreateExoWindow("org.chromium.arc.2");
  EXPECT_TRUE(controller_->GetItem(ash::ShelfID(app_id2)));

  // Check the window state in instance for app2
  instances = app_service_proxy_->InstanceRegistry().GetInstances(app_id2);
  EXPECT_EQ(1u, instances.size());
  aura::Window* window2 = (*instances.begin())->Window();
  latest_state = app_service_proxy_->InstanceRegistry().GetState(window2);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            latest_state);

  // App1 is inactive.
  latest_state = app_service_proxy_->InstanceRegistry().GetState(window1);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kVisible,
            latest_state);

  // Select the app1
  SelectItem(ash::ShelfID(app_id1));
  latest_state = app_service_proxy_->InstanceRegistry().GetState(window1);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            latest_state);
  latest_state = app_service_proxy_->InstanceRegistry().GetState(window2);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kVisible,
            latest_state);

  // Close the window for app1, and destroy the task.
  arc_window1->CloseNow();
  app_host()->OnTaskDestroyed(1);
  EXPECT_TRUE(
      app_service_proxy_->InstanceRegistry().GetInstances(app_id1).empty());

  // App2 is activated.
  latest_state = app_service_proxy_->InstanceRegistry().GetState(window2);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            latest_state);

  // destroy the task for app2 and close the window.
  app_host()->OnTaskDestroyed(2);
  arc_window2->CloseNow();
  EXPECT_TRUE(
      app_service_proxy_->InstanceRegistry().GetInstances(app_id2).empty());

  StopInstance();
}

// Test what happens when the logical window ID is provided, and some window
// might be hidden in the shelf.
IN_PROC_BROWSER_TEST_F(AppServiceAppWindowArcAppBrowserTest, LogicalWindowId) {
  // Install app to remember existing apps.
  StartInstance();
  InstallTestApps(kTestAppPackage, true, false);
  SendPackageAdded(kTestAppPackage, false);

  // Create the windows for the app.
  views::Widget* arc_window1 = CreateExoWindow("org.chromium.arc.1");
  views::Widget* arc_window2 = CreateExoWindow("org.chromium.arc.2");

  // Simulate task creation so the app is marked as running/open.
  const std::string app_id = GetTestApp1Id(kTestAppPackage);
  std::unique_ptr<ArcAppListPrefs::AppInfo> info = app_prefs()->GetApp(app_id);
  app_host()->OnTaskCreated(1, info->package_name, info->activity, info->name,
                            CreateIntentUriWithShelfGroupAndLogicalWindow(
                                "shelf_group_1", "logical_window_1"),
                            /*session_id=*/0);
  app_host()->OnTaskCreated(2, info->package_name, info->activity, info->name,
                            CreateIntentUriWithShelfGroupAndLogicalWindow(
                                "shelf_group_1", "logical_window_1"),
                            /*session_id=*/0);

  // Both windows should show up in the instance registry.
  auto instances = app_service_proxy_->InstanceRegistry().GetInstances(app_id);
  EXPECT_EQ(2u, instances.size());

  // Of those two, one should be hidden.
  auto is_hidden = [](const apps::Instance* instance) {
    return instance->Window()->GetProperty(ash::kHideInShelfKey);
  };
  EXPECT_EQ(1, base::ranges::count_if(instances, is_hidden));

  // The hidden window should be task_id 2.
  aura::Window* window1 =
      (*(base::ranges::find_if_not(instances, is_hidden)))->Window();
  aura::Window* window2 =
      (*(base::ranges::find_if(instances, is_hidden)))->Window();

  apps::InstanceState latest_state =
      app_service_proxy_->InstanceRegistry().GetState(window1);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning,
            latest_state);
  latest_state = app_service_proxy_->InstanceRegistry().GetState(window2);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning,
            latest_state);

  // If the user focuses window 2, it should become active, but still hidden in
  // the shelf.
  app_host()->OnTaskSetActive(2);
  latest_state = app_service_proxy_->InstanceRegistry().GetState(window2);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            latest_state);
  EXPECT_TRUE(window2->GetProperty(ash::kHideInShelfKey));

  // Close first window. No window should be hidden anymore.
  arc_window1->CloseNow();
  app_host()->OnTaskDestroyed(1);
  instances = app_service_proxy_->InstanceRegistry().GetInstances(app_id);
  EXPECT_EQ(1u, instances.size());
  EXPECT_EQ(0, base::ranges::count_if(instances, is_hidden));

  // Close second window.
  app_host()->OnTaskDestroyed(2);
  arc_window2->CloseNow();
  EXPECT_TRUE(
      app_service_proxy_->InstanceRegistry().GetInstances(app_id).empty());
}

// Test what happens when ARC is used to launch a payment task for a PWA app,
// as the ARC task should not be shown on the shelf.
IN_PROC_BROWSER_TEST_F(AppServiceAppWindowArcAppBrowserTest, PaymentApp) {
  // Install app to remember existing apps.
  StartInstance();
  InstallTestApps(kTestPaymentAppPackage, false, true);
  SendPackageAdded(kTestPaymentAppPackage, false);

  // Create the windows for the payment app.
  views::Widget* payment_window = CreateExoWindow("org.chromium.arc.1");

  // Simulate task creation so the app is marked as running/open.
  const std::string payment_app_id =
      GetTestPaymentAppId(kTestPaymentAppPackage);
  std::unique_ptr<ArcAppListPrefs::AppInfo> info =
      app_prefs()->GetApp(payment_app_id);
  app_host()->OnTaskCreated(1, info->package_name, info->activity, info->name,
                            info->intent_uri, /*session_id=*/0);

  // There should NOT be an entry on the shelf for the payment task
  EXPECT_FALSE(controller_->GetItem(ash::ShelfID(payment_app_id)));

  // The payment window should still show up in the instance registry.
  auto instances =
      app_service_proxy_->InstanceRegistry().GetInstances(payment_app_id);
  EXPECT_EQ(1u, instances.size());

  // The payment window should be hidden
  auto is_hidden = [](const apps::Instance* instance) {
    return instance->Window()->GetProperty(ash::kHideInShelfKey);
  };
  EXPECT_EQ(1, base::ranges::count_if(instances, is_hidden));

  // No windows should remain if we close the payment window
  payment_window->CloseNow();
  app_host()->OnTaskDestroyed(1);
  EXPECT_TRUE(app_service_proxy_->InstanceRegistry()
                  .GetInstances(payment_app_id)
                  .empty());
}

class AppServiceAppWindowSystemWebAppBrowserTest
    : public AppServiceAppWindowWebAppBrowserTest,
      public WithCrosapiParam {
  void SetUpOnMainThread() override {
    AppServiceAppWindowWebAppBrowserTest::SetUpOnMainThread();
    if (browser() == nullptr) {
      // Create a new Ash browser window so test code using browser() can work
      // even when Lacros is the only browser.
      // TODO(crbug.com/40270051): Remove uses of browser() from such tests.
      chrome::NewEmptyWindow(ProfileManager::GetActiveUserProfile());
      SelectFirstBrowser();
    }
    VerifyLacrosStatus();
  }
};

IN_PROC_BROWSER_TEST_P(AppServiceAppWindowSystemWebAppBrowserTest,
                       SystemWebAppWindow) {
  ash::SystemWebAppManager::GetForTest(browser()->profile())
      ->InstallSystemAppsForTesting();

  const std::string app_id = web_app::kOsSettingsAppId;
  web_app::LaunchWebAppBrowser(browser()->profile(), app_id);

  auto instances = app_service_proxy_->InstanceRegistry().GetInstances(app_id);
  EXPECT_EQ(1u, instances.size());
  const auto* instance = (*instances.begin()).get();
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            GetAppInstanceState(app_id, instance->Window()));

  const ash::ShelfItem& item = GetLastShelfItem();
  // Since it is already active, clicking it should minimize.
  SelectItem(item.id);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning,
            GetAppInstanceState(app_id, instance->Window()));

  // Click the item again to activate the app.
  SelectItem(item.id);
  EXPECT_EQ(apps::InstanceState::kStarted | apps::InstanceState::kRunning |
                apps::InstanceState::kActive | apps::InstanceState::kVisible,
            GetAppInstanceState(app_id, instance->Window()));

  ui_test_utils::BrowserChangeObserver browser_close_observer(
      nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kRemoved);
  controller_->Close(item.id);
  // Make sure that the window is closed.
  browser_close_observer.Wait();
  instances = app_service_proxy_->InstanceRegistry().GetInstances(app_id);
  EXPECT_TRUE(instances.empty());
}

INSTANTIATE_TEST_SUITE_P(All,
                         AppServiceAppWindowSystemWebAppBrowserTest,
                         ::testing::Values(CrosapiParam::kEnabled,
                                           CrosapiParam::kDisabled),
                         WithCrosapiParam::ParamToString);