[go: nahoru, domu]

Reland "crosier: Add ARC support"

This is a reland of commit b873bfb45ae306bf9d6de69524c6e1088f375af9

Original change's description:
> crosier: Add ARC support
>
> - Add ChromeOSIntegrationArcMixin to setup arc for tests;
> - Add AdbHelper to wrap adb commands;
> - A simple test to install an apk and create an ARC window;
>
> Bug: b:306225047
> Cq-Include-Trybots: luci.chrome.try:chromeos-betty-pi-arc-chrome
> Change-Id: I5590f0118788a1ec287d5f6fc6cb1faa5309fa7d
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5062637
> Reviewed-by: Yury Khmel <khmel@chromium.org>
> Reviewed-by: James Cook <jamescook@chromium.org>
> Commit-Queue: Xiyuan Xia <xiyuan@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1245472}

Bug: b:306225047
Change-Id: I1a6c87db282b5eb8a8f8b8371719379a2b6dcfbd
Cq-Include-Trybots: luci.chrome.try:chromeos-betty-pi-arc-chrome
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5190366
Reviewed-by: James Cook <jamescook@chromium.org>
Commit-Queue: Xiyuan Xia <xiyuan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1258659}
diff --git a/chrome/browser/ash/arc/arc_integration_test.cc b/chrome/browser/ash/arc/arc_integration_test.cc
new file mode 100644
index 0000000..0b17fb7f
--- /dev/null
+++ b/chrome/browser/ash/arc/arc_integration_test.cc
@@ -0,0 +1,56 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/constants/app_types.h"
+#include "chrome/browser/ash/login/test/session_manager_state_waiter.h"
+#include "chrome/test/base/chromeos/crosier/ash_integration_test.h"
+#include "chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.h"
+#include "chrome/test/base/chromeos/crosier/chromeos_integration_login_mixin.h"
+#include "ui/aura/client/aura_constants.h"
+#include "ui/aura/window.h"
+
+class ArcIntegrationTest : public AshIntegrationTest {
+ public:
+  ArcIntegrationTest() {
+    // This is needed to keep the browser test running after dismissing login
+    // screen. Otherwise, login screen destruction releases its ScopedKeepAlive
+    // which triggers shutdown from ShutdownIfNoBrowsers.
+    set_exit_when_last_browser_closes(false);
+
+    login_mixin().SetMode(ChromeOSIntegrationLoginMixin::Mode::kTestLogin);
+    arc_mixin().SetMode(ChromeOSIntegrationArcMixin::Mode::kEnabled);
+  }
+
+  ArcIntegrationTest(const ArcIntegrationTest&) = delete;
+  ArcIntegrationTest& operator=(const ArcIntegrationTest&) = delete;
+
+  ~ArcIntegrationTest() override = default;
+
+  // InteractiveAshTest:
+  void SetUpOnMainThread() override {
+    InteractiveAshTest::SetUpOnMainThread();
+
+    login_mixin().Login();
+    ash::test::WaitForPrimaryUserSessionStart();
+    arc_mixin().WaitForBootAndConnectAdb();
+  }
+};
+
+IN_PROC_BROWSER_TEST_F(ArcIntegrationTest, CreateWindow) {
+  // Test android apps are used by tast and deployed via "tast-local-apks-cros"
+  // package.
+  const base::FilePath kTestApk(
+      "/usr/local/libexec/tast/apks/local/cros/ArcKeyCharacterMapTest.apk");
+  ASSERT_TRUE(arc_mixin().InstallApk(kTestApk));
+
+  constexpr char kActivity[] = "org.chromium.arc.testapp.kcm.MainActivity";
+  constexpr char kPackage[] = "org.chromium.arc.testapp.kcm";
+
+  aura::Window* window =
+      arc_mixin().LaunchAndWaitForWindow(kPackage, kActivity);
+  ASSERT_NE(window, nullptr);
+
+  int window_app_type = window->GetProperty(aura::client::kAppType);
+  EXPECT_EQ(window_app_type, static_cast<int>(ash::AppType::ARC_APP));
+}
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index a5b8390..f47370b 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -247,6 +247,15 @@
       ]
 
       deps += [ "//ui/ozone/platform/drm:ui_controls" ]
+
+      if (is_chrome_branded) {
+        sources += [
+          "base/chromeos/crosier/adb_helper.cc",
+          "base/chromeos/crosier/adb_helper.h",
+          "base/chromeos/crosier/chromeos_integration_arc_mixin.cc",
+          "base/chromeos/crosier/chromeos_integration_arc_mixin.h",
+        ]
+      }
     }
 
     deps += [
@@ -1108,7 +1117,10 @@
       ]
 
       if (is_chrome_branded && is_chromeos_ash) {
-        sources += [ "base/chromeos/crosier/test_accounts_test.cc" ]
+        sources += [
+          "../browser/ash/arc/arc_integration_test.cc",
+          "base/chromeos/crosier/test_accounts_test.cc",
+        ]
       }
 
       data = [
diff --git a/chrome/test/base/chromeos/crosier/adb_helper.cc b/chrome/test/base/chromeos/crosier/adb_helper.cc
new file mode 100644
index 0000000..96f0f00
--- /dev/null
+++ b/chrome/test/base/chromeos/crosier/adb_helper.cc
@@ -0,0 +1,204 @@
+// Copyright 2024 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/test/base/chromeos/crosier/adb_helper.h"
+
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/logging.h"
+#include "base/run_loop.h"
+#include "base/strings/strcat.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "base/task/single_thread_task_runner.h"
+#include "base/threading/thread_restrictions.h"
+#include "base/time/time.h"
+#include "chrome/test/base/chromeos/crosier/helper/test_sudo_helper_client.h"
+
+namespace {
+
+// Android vendtor key to authorize adb connection.
+constexpr char kArcKey[] = R"(-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCnHNzujonYRLoI
+F2pyJX1SSrqmiT/3rTRCP1X0pj1V/sPGwgvIr+3QjZehLUGRQL0wneBNXd6EVrST
+drO4cOPwSxRJjCf+/PtS1nwkz+o/BGn5yhNppdSro7aPoQxEVM8qLtN5Ke9tx/zE
+ggxpF8D3XBC6Los9lAkyesZI6xqXESeofOYu3Hndzfbz8rAjC0X+p6Sx561Bt1dn
+T7k2cP0mwWfITjW8tAhzmKgL4tGcgmoLhMHl9JgScFBhW2Nd0QAR4ACyVvryJ/Xa
+2L6T2YpUjqWEDbiJNEApFb+m+smIbyGz0H/Kj9znoRs84z3/8rfyNQOyf7oqBpr2
+52XG4totAgMBAAECggEARisKYWicXKDO9CLQ4Uj4jBswsEilAVxKux5Y+zbqPjeR
+AN3tkMC+PHmXl2enRlRGnClOS24ExtCZVenboLBWJUmBJTiieqDC7o985QAgPYGe
+9fFxoUSuPbuqJjjbK73olq++v/tpu1Djw6dPirkcn0CbDXIJqTuFeRqwM2H0ckVl
+mVGUDgATckY0HWPyTBIzwBYIQTvAYzqFHmztcUahQrfi9XqxnySI91no8X6fR323
+R8WQ44atLWO5TPCu5JEHCwuTzsGEG7dEEtRQUxAsH11QC7S53tqf10u40aT3bXUh
+XV62ol9Zk7h3UrrlT1h1Ae+EtgIbhwv23poBEHpRQQKBgQDeUJwLfWQj0xHO+Jgl
+gbMCfiPYvjJ9yVcW4ET4UYnO6A9bf0aHOYdDcumScWHrA1bJEFZ/cqRvqUZsbSsB
++thxa7gjdpZzBeSzd7M+Ygrodi6KM/ojSQMsen/EbRFerZBvsXimtRb88NxTBIW1
+RXRPLRhHt+VYEF/wOVkNZ5c2eQKBgQDAbwNkkVFTD8yQJFxZZgr1F/g/nR2IC1Yb
+ylusFztLG998olxUKcWGGMoF7JjlM6pY3nt8qJFKek9bRJqyWSqS4/pKR7QTU4Nl
+a+gECuD3f28qGFgmay+B7Fyi9xmBAsGINyVxvGyKH95y3QICw1V0Q8uuNwJW2feo
+3+UD2/rkVQKBgFloh+ljC4QQ3gekGOR0rf6hpl8D1yCZecn8diB8AnVRBOQiYsX9
+j/XDYEaCDQRMOnnwdSkafSFfLbBrkzFfpe6viMXSap1l0F2RFWhQW9yzsvHoB4Br
+W7hmp73is2qlWQJimIhLKiyd3a4RkoidnzI8i5hEUBtDsqHVHohykfDZAoGABNhG
+q5eFBqRVMCPaN138VKNf2qon/i7a4iQ8Hp8PHRr8i3TDAlNy56dkHrYQO2ULmuUv
+Erpjvg5KRS/6/RaFneEjgg9AF2R44GrREpj7hP+uWs72GTGFpq2+v1OdTsQ0/yr0
+RGLMEMYwoY+y50Lnud+jFyXHZ0xhkdzhNTGqpWkCgYBigHVt/p8uKlTqhlSl6QXw
+1AyaV/TmfDjzWaNjmnE+DxQfXzPi9G+cXONdwD0AlRM1NnBRN+smh2B4RBeU515d
+x5RpTRFgzayt0I4Rt6QewKmAER3FbbPzaww2pkfH1zr4GJrKQuceWzxUf46K38xl
+yee+dcuGhs9IGBOEEF7lFA==
+-----END PRIVATE KEY-----)";
+
+// Install the vendor key in the given path and return the full path to the key
+// file.
+base::FilePath InstallVendorKey(const base::FilePath& path) {
+  base::FilePath key_file = path.Append("crosier.adb_key");
+  base::WriteFile(key_file, kArcKey, std::size(kArcKey));
+
+  constexpr char command_template[] = R"(
+    KEY_DIR="%s"
+    chown -R root.root "$KEY_DIR"
+    chmod 0755 "$KEY_DIR"
+    chmod 0600 "%s"
+  )";
+  auto result = TestSudoHelperClient().RunCommand(base::StringPrintf(
+      command_template, path.value().c_str(), key_file.value().c_str()));
+  CHECK_EQ(result.return_code, 0);
+
+  return key_file;
+}
+
+// Removes the vendor key files.
+void RemoveVendorKey(base::ScopedTempDir dir) {
+  if (!dir.IsValid()) {
+    return;
+  }
+
+  constexpr char command_template[] = R"(
+     rm -rf "%s"
+  )";
+  auto result = TestSudoHelperClient().RunCommand(
+      base::StringPrintf(command_template, dir.Take().value().c_str()));
+  CHECK_EQ(result.return_code, 0);
+}
+
+void KillServer() {
+  // `adb kill-server` is not reliable (crbug.com/855325).
+  // Not using `killall` since it can wait for orphan adb processes indefinitely
+  // (b/137797801).
+  //
+  // Use `kill -9` directly.
+  TestSudoHelperClient().RunCommand(R"(
+     while pgrep adb; do
+       kill -9 $(pgrep adb)
+       sleep 0.1
+     done
+  )");
+}
+
+void GiveItSomeTime(base::TimeDelta t) {
+  base::RunLoop run_loop;
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
+      FROM_HERE, run_loop.QuitClosure(), t);
+  run_loop.Run();
+}
+
+}  // namespace
+
+AdbHelper::AdbHelper() = default;
+
+AdbHelper::~AdbHelper() {
+  if (!initialized_) {
+    return;
+  }
+
+  KillServer();
+
+  base::ScopedAllowBlockingForTesting allow_blocking;
+  RemoveVendorKey(std::move(vendor_key_dir_));
+}
+
+void AdbHelper::Intialize() {
+  initialized_ = true;
+
+  KillServer();
+
+  base::ScopedAllowBlockingForTesting allow_blocking;
+  CHECK(vendor_key_dir_.CreateUniqueTempDir())
+      << "Failed to create temp dir to hold vendor key.";
+  vendor_key_file_ = InstallVendorKey(vendor_key_dir_.GetPath());
+  CHECK(!vendor_key_file_.empty());
+
+  constexpr char command_template[] = R"(
+      ADB_VENDOR_KEYS=%s adb start-server
+  )";
+  auto result = TestSudoHelperClient().RunCommand(
+      base::StringPrintf(command_template, vendor_key_file_.value().c_str()));
+  CHECK_EQ(result.return_code, 0);
+
+  WaitForDevice();
+  CHECK(!serial_.empty());
+}
+
+bool AdbHelper::InstallApk(const base::FilePath& apk_path) {
+  {
+    base::ScopedAllowBlockingForTesting allow_blocking;
+    if (!base::PathExists(apk_path)) {
+      LOG(ERROR) << "Apk file does not exist: " << apk_path;
+      return false;
+    }
+  }
+
+  // Disable apk check.
+  auto result = TestSudoHelperClient().RunCommand(R"(
+    adb shell settings put global verifier_verify_adb_installs 0
+  )");
+  CHECK_EQ(result.return_code, 0);
+
+  // Install the apk.
+  return Command(
+      base::StringPrintf(R"(install "%s")", apk_path.value().c_str()));
+}
+
+bool AdbHelper::Command(const std::string_view command) {
+  auto result = TestSudoHelperClient().RunCommand(
+      base::StrCat({"ADB_VENDOR_KEYS=", vendor_key_file_.value(), " adb -s ",
+                    serial_, " ", command}));
+  return result.return_code == 0;
+}
+
+void AdbHelper::WaitForDevice() {
+  constexpr char kEmulatorPrefix[] = "emulator-";
+  constexpr char kStateDevice[] = "device";
+
+  constexpr char command_template[] = R"(
+    ADB_VENDOR_KEYS=%s adb devices
+  )";
+  const std::string adb_devices =
+      base::StringPrintf(command_template, vendor_key_file_.value().c_str());
+
+  do {
+    GiveItSomeTime(base::Milliseconds(500));
+
+    // List all devices.
+    auto result = TestSudoHelperClient().RunCommand(adb_devices);
+    CHECK_EQ(result.return_code, 0);
+
+    // Search for the first emulator device with "device" state.
+    const auto lines = base::SplitString(
+        result.output, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
+    for (const auto& line : lines) {
+      const auto fields = base::SplitString(line, " \t", base::TRIM_WHITESPACE,
+                                            base::SPLIT_WANT_NONEMPTY);
+      if (fields.size() != 2) {
+        continue;
+      }
+
+      if (base::StartsWith(fields[0], kEmulatorPrefix) &&
+          fields[1] == kStateDevice) {
+        serial_ = fields[0];
+        break;
+      }
+    }
+  } while (serial_.empty());
+}
diff --git a/chrome/test/base/chromeos/crosier/adb_helper.h b/chrome/test/base/chromeos/crosier/adb_helper.h
new file mode 100644
index 0000000..00dc8161
--- /dev/null
+++ b/chrome/test/base/chromeos/crosier/adb_helper.h
@@ -0,0 +1,48 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_TEST_BASE_CHROMEOS_CROSIER_ADB_HELPER_H_
+#define CHROME_TEST_BASE_CHROMEOS_CROSIER_ADB_HELPER_H_
+
+#include <string_view>
+
+#include "base/files/scoped_temp_dir.h"
+#include "chrome/test/base/chromeos/crosier/helper/test_sudo_helper_client.h"
+
+namespace base {
+class FilePath;
+}
+
+// Helper to run adb command via the TestSudoHelper.
+class AdbHelper {
+ public:
+  AdbHelper();
+  AdbHelper(const AdbHelper&) = delete;
+  AdbHelper& operator=(const AdbHelper&) = delete;
+  ~AdbHelper();
+
+  // Starts adb server and connect to the first "emulator" device.
+  void Intialize();
+
+  // Installs the apk at the given path on the DUT.
+  bool InstallApk(const base::FilePath& apk_path);
+
+  // Runs the given command via adb.
+  bool Command(const std::string_view command);
+
+ private:
+  // Waits for the first emulator device to be ready and extract serial.
+  void WaitForDevice();
+
+  bool initialized_ = false;
+
+  // Device serial that is passed with "-s" to adb.
+  std::string serial_;
+
+  // A temp dir to store Android vendor keys.
+  base::ScopedTempDir vendor_key_dir_;
+  base::FilePath vendor_key_file_;
+};
+
+#endif  // CHROME_TEST_BASE_CHROMEOS_CROSIER_ADB_HELPER_H_
diff --git a/chrome/test/base/chromeos/crosier/ash_integration_test.cc b/chrome/test/base/chromeos/crosier/ash_integration_test.cc
index d662c7e..3c609290 100644
--- a/chrome/test/base/chromeos/crosier/ash_integration_test.cc
+++ b/chrome/test/base/chromeos/crosier/ash_integration_test.cc
@@ -27,6 +27,9 @@
 
 namespace {
 
+// A dir on DUT to host wayland socket and arc-bridge sockets.
+inline constexpr char kRunChrome[] = "/run/chrome";
+
 // Simulates a failure for a Gaia URL request.
 std::unique_ptr<net::test_server::HttpResponse> HandleGaiaURL(
     const net::test_server::HttpRequest& request) {
@@ -44,14 +47,6 @@
   CHECK(command_line);
 
   OverrideGaiaUrlForLacros(command_line);
-
-  // Enable the Wayland server.
-  command_line->AppendSwitch(ash::switches::kAshEnableWaylandServer);
-
-  // Set up XDG_RUNTIME_DIR for Wayland.
-  std::unique_ptr<base::Environment> env(base::Environment::Create());
-  CHECK(scoped_temp_dir_xdg_.CreateUniqueTempDir());
-  env->SetVar("XDG_RUNTIME_DIR", scoped_temp_dir_xdg_.GetPath().AsUTF8Unsafe());
 }
 
 void AshIntegrationTest::SetUpLacrosBrowserManager() {
@@ -67,9 +62,9 @@
 void AshIntegrationTest::WaitForAshFullyStarted() {
   CHECK(base::CommandLine::ForCurrentProcess()->HasSwitch(
       ash::switches::kAshEnableWaylandServer))
-      << "Did you forget to call SetUpCommandLineForLacros?";
+      << "Wayland server should be enabled.";
   base::ScopedAllowBlockingForTesting allow_blocking;
-  base::FilePath xdg_path = scoped_temp_dir_xdg_.GetPath();
+  base::FilePath xdg_path(kRunChrome);
   base::RepeatingTimer timer;
   base::RunLoop run_loop1;
   timer.Start(FROM_HERE, base::Milliseconds(100),
@@ -97,6 +92,17 @@
   CHECK(extra_parts->did_post_browser_start());
 }
 
+void AshIntegrationTest::SetUpCommandLine(base::CommandLine* command_line) {
+  InteractiveAshTest::SetUpCommandLine(command_line);
+
+  // Enable the Wayland server.
+  command_line->AppendSwitch(ash::switches::kAshEnableWaylandServer);
+
+  // Set up XDG_RUNTIME_DIR for Wayland.
+  std::unique_ptr<base::Environment> env(base::Environment::Create());
+  env->SetVar("XDG_RUNTIME_DIR", kRunChrome);
+}
+
 void AshIntegrationTest::SetUpOnMainThread() {
   InteractiveAshTest::SetUpOnMainThread();
 
diff --git a/chrome/test/base/chromeos/crosier/ash_integration_test.h b/chrome/test/base/chromeos/crosier/ash_integration_test.h
index 18dbe29b..828e017 100644
--- a/chrome/test/base/chromeos/crosier/ash_integration_test.h
+++ b/chrome/test/base/chromeos/crosier/ash_integration_test.h
@@ -12,6 +12,10 @@
 #include "chrome/test/base/chromeos/crosier/chromeos_integration_test_mixin.h"
 #include "chrome/test/base/chromeos/crosier/interactive_ash_test.h"
 
+#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
+#include "chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.h"
+#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
+
 namespace base {
 class CommandLine;
 }
@@ -51,10 +55,15 @@
   void WaitForAshFullyStarted();
 
   // MixinBasedInProcessBrowserTest:
+  void SetUpCommandLine(base::CommandLine* command_line) override;
   void SetUpOnMainThread() override;
 
   ChromeOSIntegrationLoginMixin& login_mixin() { return login_mixin_; }
 
+#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
+  ChromeOSIntegrationArcMixin& arc_mixin() { return arc_mixin_; }
+#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
+
  private:
   // Overrides the Gaia URL to point to a local test server that produces an
   // error, which is expected behavior in test environments.
@@ -67,8 +76,10 @@
   // Login support.
   ChromeOSIntegrationLoginMixin login_mixin_{&mixin_host_};
 
-  // Directory used by Wayland/Lacros in environment variable XDG_RUNTIME_DIR.
-  base::ScopedTempDir scoped_temp_dir_xdg_;
+#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
+  // ARC is only supported on the branded build.
+  ChromeOSIntegrationArcMixin arc_mixin_{&mixin_host_, login_mixin_};
+#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
 
   std::unique_ptr<net::test_server::EmbeddedTestServer> https_server_;
 };
diff --git a/chrome/test/base/chromeos/crosier/ash_integration_test_test.cc b/chrome/test/base/chromeos/crosier/ash_integration_test_test.cc
index 36dafda7..8964566 100644
--- a/chrome/test/base/chromeos/crosier/ash_integration_test_test.cc
+++ b/chrome/test/base/chromeos/crosier/ash_integration_test_test.cc
@@ -11,7 +11,7 @@
 class AshIntegrationTestTest : public AshIntegrationTest {
  public:
   void SetUpCommandLine(base::CommandLine* command_line) override {
-    InteractiveAshTest::SetUpCommandLine(command_line);
+    AshIntegrationTest::SetUpCommandLine(command_line);
     SetUpCommandLineForLacros(command_line);
   }
 };
diff --git a/chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.cc b/chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.cc
new file mode 100644
index 0000000..e91f2b8
--- /dev/null
+++ b/chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.cc
@@ -0,0 +1,278 @@
+// Copyright 2024 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/test/base/chromeos/crosier/chromeos_integration_arc_mixin.h"
+
+#include "ash/components/arc/metrics/arc_metrics_constants.h"
+#include "ash/constants/ash_switches.h"
+#include "ash/public/cpp/window_properties.h"
+#include "ash/shell.h"
+#include "ash/test/active_window_waiter.h"
+#include "base/command_line.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/run_loop.h"
+#include "base/scoped_observation.h"
+#include "base/strings/string_util.h"
+#include "chrome/browser/ash/app_list/arc/arc_app_list_prefs.h"
+#include "chrome/browser/ash/app_list/arc/arc_app_utils.h"
+#include "chrome/browser/ash/arc/boot_phase_monitor/arc_boot_phase_monitor_bridge.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/test/base/chromeos/crosier/chromeos_integration_login_mixin.h"
+#include "chrome/test/base/chromeos/crosier/helper/test_sudo_helper_client.h"
+#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
+#include "components/user_manager/user_manager.h"
+#include "content/public/browser/browser_context.h"
+#include "ui/aura/client/aura_constants.h"
+#include "ui/aura/window.h"
+#include "ui/aura/window_observer.h"
+#include "ui/events/event_constants.h"
+
+namespace {
+
+// ArcBootWaiter waits for boot completed from `ArcBootPhaseMonitorBridge`.
+class ArcBootWaiter : public arc::ArcBootPhaseMonitorBridge::Observer {
+ public:
+  ArcBootWaiter() = default;
+  ~ArcBootWaiter() override = default;
+
+  void Wait() {
+    const user_manager::User* primary_user =
+        user_manager::UserManager::Get()->GetPrimaryUser();
+    CHECK(primary_user);
+
+    auto* browser_context =
+        ash::BrowserContextHelper::Get()->GetBrowserContextByUser(primary_user);
+    arc::ArcBootPhaseMonitorBridge* boot_bridge =
+        arc::ArcBootPhaseMonitorBridge::GetForBrowserContext(browser_context);
+    CHECK(boot_bridge);
+
+    scoped_observation_.Observe(boot_bridge);
+
+    wait_loop_.Run();
+  }
+
+  // arc::ArcBootPhaseMonitorBridge::Observer:
+  void OnBootCompleted() override { wait_loop_.Quit(); }
+
+ private:
+  base::RunLoop wait_loop_;
+  base::ScopedObservation<arc::ArcBootPhaseMonitorBridge,
+                          arc::ArcBootPhaseMonitorBridge::Observer>
+      scoped_observation_{this};
+};
+
+// AppReadyWaiter waits until the given `app_id` is ready and launchable.
+class AppReadyWaiter : public ArcAppListPrefs::Observer {
+ public:
+  AppReadyWaiter(ArcAppListPrefs* arc_app_list_prefs,
+                 const std::string_view app_id)
+      : prefs_(arc_app_list_prefs), app_id_(app_id) {
+    scoped_observation_.Observe(arc_app_list_prefs);
+  }
+
+  void Wait() {
+    if (IsAppReadyAndLaunchable()) {
+      return;
+    }
+
+    wait_loop_.Run();
+
+    CHECK(IsAppReadyAndLaunchable());
+  }
+
+  // ArcAppListPrefs::Observer:
+  void OnAppRegistered(const std::string& app_id,
+                       const ArcAppListPrefs::AppInfo& app_info) override {
+    if (app_id == app_id_ && IsAppReadyAndLaunchable()) {
+      wait_loop_.Quit();
+    }
+  }
+  void OnAppStatesChanged(const std::string& app_id,
+                          const ArcAppListPrefs::AppInfo& app_info) override {
+    if (app_id == app_id_ && IsAppReadyAndLaunchable()) {
+      wait_loop_.Quit();
+    }
+  }
+
+ private:
+  bool IsAppReadyAndLaunchable() const {
+    auto app_info = prefs_->GetApp(app_id_);
+    if (!app_info) {
+      return false;
+    }
+
+    return app_info->ready && app_info->launchable;
+  }
+
+  const raw_ptr<ArcAppListPrefs> prefs_;
+  const std::string app_id_;
+  base::ScopedObservation<ArcAppListPrefs, ArcAppListPrefs::Observer>
+      scoped_observation_{this};
+  base::RunLoop wait_loop_;
+};
+
+// WindowAppIdWaiter waits for `ash::kAppIDKey` property set on a given window.
+class WindowAppIdWaiter : public aura::WindowObserver {
+ public:
+  explicit WindowAppIdWaiter(aura::Window* window) {
+    observation_.Observe(window);
+  }
+
+  const std::string* Wait() {
+    found_app_id_ = observation_.GetSource()->GetProperty(ash::kAppIDKey);
+    if (!found_app_id_) {
+      run_loop_.Run();
+    }
+
+    return found_app_id_.get();
+  }
+
+  // aura::WindowObserver:
+  void OnWindowPropertyChanged(aura::Window* window,
+                               const void* key,
+                               intptr_t old) override {
+    if (key != ash::kAppIDKey) {
+      return;
+    }
+
+    found_app_id_ = window->GetProperty(ash::kAppIDKey);
+    run_loop_.Quit();
+  }
+  void OnWindowDestroyed(aura::Window* window) override {
+    observation_.Reset();
+    run_loop_.Quit();
+  }
+
+ private:
+  base::RunLoop run_loop_;
+  raw_ptr<std::string> found_app_id_ = nullptr;
+  base::ScopedObservation<aura::Window, aura::WindowObserver> observation_{
+      this};
+};
+
+// Returns whether ARCVM should be used based on tast_use_flags.txt.
+bool ShouldEnableArcVm() {
+  base::ScopedAllowBlockingForTesting allow_blocking;
+  std::string use_flags;
+  CHECK(base::ReadFileToString(
+      base::FilePath("/usr/local/etc/tast_use_flags.txt"), &use_flags));
+  return base::Contains(use_flags, "arcvm") &&
+         !base::Contains(use_flags, "arcpp");
+}
+
+// Gets the active user's browser context.
+content::BrowserContext* GetActiveUserBrowserContext() {
+  auto* user = user_manager::UserManager::Get()->GetActiveUser();
+  return ash::BrowserContextHelper::Get()->GetBrowserContextByUser(user);
+}
+
+void WaitForAppRegister(const std::string& app_id) {
+  AppReadyWaiter(ArcAppListPrefs::Get(GetActiveUserBrowserContext()), app_id)
+      .Wait();
+}
+
+}  // namespace
+
+ChromeOSIntegrationArcMixin::ChromeOSIntegrationArcMixin(
+    InProcessBrowserTestMixinHost* host,
+    const ChromeOSIntegrationLoginMixin& login_mixin)
+    : InProcessBrowserTestMixin(host), login_mixin_(login_mixin) {}
+
+ChromeOSIntegrationArcMixin::~ChromeOSIntegrationArcMixin() = default;
+
+void ChromeOSIntegrationArcMixin::SetMode(Mode mode) {
+  CHECK(!setup_called_);
+  mode_ = mode;
+}
+
+void ChromeOSIntegrationArcMixin::SetUp() {
+  setup_called_ = true;
+}
+
+void ChromeOSIntegrationArcMixin::WaitForBootAndConnectAdb() {
+  ArcBootWaiter().Wait();
+  adb_helper_.Intialize();
+
+  const bool needs_play_store = (mode_ == Mode::kSupported);
+  if (!needs_play_store) {
+    // Disable play store. Otherwise it crashes.
+    CHECK(adb_helper_.Command(
+        "shell pm disable-user --user 0 com.android.vending"));
+  }
+}
+
+bool ChromeOSIntegrationArcMixin::InstallApk(const base::FilePath& apk_path) {
+  return adb_helper_.InstallApk(apk_path);
+}
+
+aura::Window* ChromeOSIntegrationArcMixin::LaunchAndWaitForWindow(
+    const std::string& package,
+    const std::string& activity) {
+  const std::string app_id = ArcAppListPrefs::GetAppId(package, activity);
+  WaitForAppRegister(app_id);
+
+  // Launch the given activity.
+  CHECK(arc::LaunchApp(GetActiveUserBrowserContext(), app_id, ui::EF_NONE,
+                       arc::UserInteractionType::NOT_USER_INITIATED));
+
+  // Wait for the activity window to be activated.
+  aura::Window* const window =
+      ash::ActiveWindowWaiter(ash::Shell::GetPrimaryRootWindow()).Wait();
+  const std::string* window_app_id = WindowAppIdWaiter(window).Wait();
+  CHECK(window_app_id);
+  CHECK(!window_app_id->empty());
+  CHECK_EQ(*window_app_id, app_id);
+  return window;
+}
+
+void ChromeOSIntegrationArcMixin::SetUpCommandLine(
+    base::CommandLine* command_line) {
+  if (mode_ == Mode::kNone) {
+    command_line->AppendSwitchASCII(ash::switches::kArcAvailability, "none");
+    return;
+  }
+
+  CHECK(login_mixin_.mode() != ChromeOSIntegrationLoginMixin::Mode::kStubLogin)
+      << "ARC does not work with stub login.";
+
+  // User data dir needs to be "/home/chronos". Otherwise,
+  // `IsArcCompatibleFileSystemUsedForUser()` returns false and ARC could not be
+  // enabled.
+  command_line->AppendSwitchASCII(::switches::kUserDataDir, "/home/chronos");
+
+  if (ShouldEnableArcVm()) {
+    command_line->AppendSwitch(ash::switches::kEnableArcVm);
+  }
+
+  // Common setup for both "Enabled" and "Supported" modes. The switches here
+  // are from "tast-tests/cros/local/chrome/internal/setup/restart.go".
+  // Reference: http://shortn/_P4IIm7c7aY
+  command_line->AppendSwitch(ash::switches::kArcDisableAppSync);
+  command_line->AppendSwitch(ash::switches::kArcDisablePlayAutoInstall);
+  command_line->AppendSwitch(ash::switches::kArcDisableLocaleSync);
+  command_line->AppendSwitchASCII(ash::switches::kArcPlayStoreAutoUpdate,
+                                  "off");
+  command_line->AppendSwitch(ash::switches::kArcDisableMediaStoreMaintenance);
+  command_line->AppendSwitch(ash::switches::kDisableArcCpuRestriction);
+
+  if (mode_ == Mode::kEnabled) {
+    command_line->AppendSwitch(ash::switches::kDisableArcOptInVerification);
+    command_line->AppendSwitchASCII(ash::switches::kArcStartMode,
+                                    "always-start-with-no-play-store");
+
+    // The "installed" mode needs `kEnableArcFeature` to work.
+    // See "IsArcAvailable()" in ash/components/arc/arc_util.cc.
+    command_line->AppendSwitchASCII(ash::switches::kArcAvailability,
+                                    "installed");
+    scoped_feature_list_.emplace();
+    scoped_feature_list_->InitFromCommandLine("EnableARC", base::EmptyString());
+  }
+
+  if (mode_ == Mode::kSupported) {
+    command_line->AppendSwitchASCII(ash::switches::kArcAvailability,
+                                    "officially-supported");
+  }
+}
diff --git a/chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.h b/chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.h
new file mode 100644
index 0000000..eb1e1b3
--- /dev/null
+++ b/chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.h
@@ -0,0 +1,76 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_TEST_BASE_CHROMEOS_CROSIER_CHROMEOS_INTEGRATION_ARC_MIXIN_H_
+#define CHROME_TEST_BASE_CHROMEOS_CROSIER_CHROMEOS_INTEGRATION_ARC_MIXIN_H_
+
+#include <string>
+
+#include "base/test/scoped_feature_list.h"
+#include "chrome/test/base/chromeos/crosier/adb_helper.h"
+#include "chrome/test/base/mixin_based_in_process_browser_test.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+namespace aura {
+class Window;
+}
+
+namespace base {
+class FilePath;
+}
+
+class ChromeOSIntegrationLoginMixin;
+
+// ChromeOSIntegrationArcMixin provides ARC support to ChromeOS integration
+// test.
+class ChromeOSIntegrationArcMixin : public InProcessBrowserTestMixin {
+ public:
+  enum class Mode {
+    // ARC is not enabled.
+    kNone,
+
+    // ARC is enabled with no play store.
+    kEnabled,
+
+    // Full ARC support. ARC is enabled and could perform play store optin. This
+    // requires Gaia identity.
+    // TODO(b/306225047): Implement this.
+    kSupported,
+  };
+
+  ChromeOSIntegrationArcMixin(InProcessBrowserTestMixinHost* host,
+                              const ChromeOSIntegrationLoginMixin& login_mixin);
+  ChromeOSIntegrationArcMixin(const ChromeOSIntegrationArcMixin&) = delete;
+  ChromeOSIntegrationArcMixin& operator=(const ChromeOSIntegrationArcMixin&) =
+      delete;
+  ~ChromeOSIntegrationArcMixin() override;
+
+  // Sets the ARC mode. Must be called before SetUp.
+  void SetMode(Mode mode);
+
+  // Waits for ARC boot_completed and connect adb.
+  void WaitForBootAndConnectAdb();
+
+  // Installs the apk at the given path on the DUT.
+  bool InstallApk(const base::FilePath& apk_path);
+
+  // Launches the give activity and wait for its window to be activated.
+  aura::Window* LaunchAndWaitForWindow(const std::string& package,
+                                       const std::string& activity);
+
+  // InProcessBrowserTestMixin:
+  void SetUp() override;
+  void SetUpCommandLine(base::CommandLine* command_line) override;
+
+ private:
+  const ChromeOSIntegrationLoginMixin& login_mixin_;
+
+  bool setup_called_ = false;
+  Mode mode_ = Mode::kNone;
+  absl::optional<base::test::ScopedFeatureList> scoped_feature_list_;
+
+  AdbHelper adb_helper_;
+};
+
+#endif  // CHROME_TEST_BASE_CHROMEOS_CROSIER_CHROMEOS_INTEGRATION_ARC_MIXIN_H_
diff --git a/chrome/test/base/chromeos/crosier/helper/test_sudo_helper.py b/chrome/test/base/chromeos/crosier/helper/test_sudo_helper.py
index eb26b6e..8421838 100755
--- a/chrome/test/base/chromeos/crosier/helper/test_sudo_helper.py
+++ b/chrome/test/base/chromeos/crosier/helper/test_sudo_helper.py
@@ -26,6 +26,7 @@
 import logging
 import os
 from pathlib import Path
+import resource
 import socket
 import subprocess
 import sys
@@ -136,19 +137,45 @@
 
     def run(self):
         try:
+            container_root_dir = "/run/containers"
+            os.makedirs(container_root_dir, mode=0o755, exist_ok=True)
+
+            sm_env = {}
+            sm_env["CONTAINER_ROOT_DIR"] = container_root_dir
+
+            # Set limits etc before execute session_manager. This should match
+            # the limits in `ui.conf`.
+            def preexec():
+                resource.setrlimit(resource.RLIMIT_NICE, (40, 40))
+                resource.setrlimit(resource.RLIMIT_RTPRIO, (10, 10))
+
             args = [
+                "/usr/bin/runcon",
+                "-t",
+                "cros_session_manager",
                 "/sbin/session_manager",
                 ("--chrome-command=%s" % str(THIS_FILE.parent / "fake_chrome")),
             ]
             logging.info("Starting session manager: args=%s", str(args))
             self._session_manager_proc = subprocess.Popen(
-                args, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
+                args,
+                stdout=subprocess.DEVNULL,
+                stderr=subprocess.STDOUT,
+                cwd="/",
+                env=sm_env,
+                preexec_fn=preexec)
 
             _wait_for_fake_chrome()
             _send_code_and_string(self._sock, 0, "started")
         except Exception as e:
             logging.error("Exception: %s", e)
-            _send_code_and_string(self._sock, 0xFF, str(e))
+
+            # Ignore BrokenPipeError since the client might be gone already.
+            try:
+                _send_code_and_string(self._sock, 0xFF, str(e))
+            except BrokenPipeError:
+                pass
+
             self._session_manager_proc = None
             return
 
@@ -166,7 +193,12 @@
 
         self._session_manager_proc = None
 
-        _send_code_and_string(self._sock, 0, "stopped")
+        # Ignore BrokenPipeError since the client might be gone already.
+        try:
+            _send_code_and_string(self._sock, 0, "stopped")
+        except BrokenPipeError:
+            pass
+
         self._sock.close()
 
     def stop(self):