Define a context manager to launch an app

Issue: INFRA-238
Change-Id: I429896a6feaf1db19ac59a6d6bcffaba26b80d81
diff --git a/deploy.py b/deploy.py
index a24ca86..b49967b 100755
--- a/deploy.py
+++ b/deploy.py
@@ -1,5 +1,6 @@
 #!/usr/bin/env python3
 
+import contextlib
 import dataclasses
 import logging
 import pathlib
@@ -39,6 +40,10 @@
         super(DeviceCommandError, self).__init__(message)
 
 
+class InvalidIntentError(DeviceCommandError):
+    """An intent was rejected by a device."""
+
+
 def adb(*args, serial=None, raise_on_error=True):
     """Run ADB command attached to serial.
 
@@ -133,17 +138,11 @@
     This simplifies UI automation. Disabling the feature globally is more robust
     than clicking through the the Privacy Impact screen per app.
     """
-    print("Disabling the Privacy Impact screen…")
-    device.adb(
-        "shell",
-        (
-            "am start -a android.intent.action.MAIN "
-            "com.fairphone.privacyimpact/.PrivacyImpactPreferenceActivity"
-        ),
-    )
-    disable_privacy_impact_checkbox = device.ui(className="android.widget.CheckBox")
-    if not disable_privacy_impact_checkbox.checked:
-        disable_privacy_impact_checkbox.click()
+    _LOG.info("Disable the Privacy Impact screen")
+    with device.launch("com.fairphone.privacyimpact/.PrivacyImpactPreferenceActivity"):
+        disable_privacy_impact_checkbox = device.ui(className="android.widget.CheckBox")
+        if not disable_privacy_impact_checkbox.checked:
+            disable_privacy_impact_checkbox.click()
 
 
 # Grant the permissions through the UI
@@ -339,6 +338,66 @@
         command.append(str(target))
         self.adb(*command)
 
+    @contextlib.contextmanager
+    def launch(self, package_or_activity: str):
+        """Launch an app and eventually goes back to the home screen.
+
+        There are two ways to start an application:
+
+        1. Start the main activity (advertised in the launcher), only
+           the package name is needed (e.g. `com.android.settings`);
+        2. Start a specific activity, the qualified activity name is
+           needed (e.g. `com.android.settings/.Settings`).
+
+        Example:
+
+        >>> device = DeviceUnderTest(...)
+        >>> with device.launch("com.android.settings"):
+        ...     device.ui(text="Bluetooth").exists
+        True
+        >>> device.ui(text="Bluetooth").exists
+        False
+
+        :param package_or_activity: The package name or the qualified
+            activity name.
+        :raise DeviceCommandError: If the app manager command failed.
+        :raise InvalidIntentError: If the package or activity could not
+            be resolved.
+        """
+        if "/" in package_or_activity:
+            [package, activity] = package_or_activity.split("/", maxsplit=1)
+        else:
+            package, activity = package_or_activity, ""
+
+        if activity:
+            command = [
+                "shell",
+                "am",
+                "start",
+                "-a",
+                "android.intent.action.MAIN",
+                package_or_activity,
+            ]
+        else:
+            # Use the monkey to avoid specifying the exact activity name.
+            command = [
+                "shell",
+                "monkey",
+                "-p",
+                package,
+                "-c",
+                "android.intent.category.LAUNCHER",
+                "1",
+            ]
+
+        try:
+            process = self.adb(*command)
+            if process.stderr:
+                raise InvalidIntentError(self.serial, " ".join(command), process.stderr)
+            yield
+        finally:
+            self.ui.press.home()
+
 
 @dataclasses.dataclass(frozen=True)
 class AndroidApp:
@@ -511,20 +570,9 @@
             # Push the scenarios, their data, and install the apps
             suite.deploy(device)
 
-            # Start the viser app
-            device.adb(
-                "shell",
-                "monkey",
-                "-p",
-                "com.lunarlabs.panda",
-                "-c",
-                "android.intent.category.LAUNCHER",
-                "1",
-            )
-
-            configure_perms(device)
-
-            configure_settings(device)
+            with device.launch("com.lunarlabs.panda"):
+                configure_perms(device)
+                configure_settings(device)
         except (HostCommandError, DeviceCommandError, uiautomator.JsonRPCError):
             _LOG.error("Failed to execute deployment", exc_info=True)
         finally: