Fix pushing the scenarios and data to wrong paths

Abstract the `adb push` command to obtain consistent results, no matter
if we want to push files, directories, or empty directories.

Work around the `adb push` command limitations to be able to target a
directory with a different name than the source directory.

Use the directory names on the device as expected by the viSer app.

Issue: INFRA-227
Change-Id: I8d168ae101d44226d7bcd45a2b2e41e23dee4950
diff --git a/deploy.py b/deploy.py
index 0ac8ecd..f9d1fc1 100755
--- a/deploy.py
+++ b/deploy.py
@@ -256,11 +256,11 @@
 
     # Copy the scenarios
     print("Pushing scenarios from `{}`…".format(scenarios_dir))
-    device.adb("push", scenarios_dir, "/sdcard/viser")
+    device.push(scenarios_dir, pathlib.Path("/sdcard/Viser"))
 
     # Copy the scenarios data
     print("Pushing scenarios data from `{}`…".format(data_dir))
-    device.adb("push", data_dir, "/sdcard/viser/data")
+    device.push(data_dir, pathlib.Path("/sdcard/Viser/Data"))
 
     # Install the smartviser apps (starting with the proxy app)
     for app in [proxy_apk] + PREBUILT_APKS:
@@ -418,6 +418,38 @@
 
         self.adb(*command, str(apk))
 
+    def push(self, source: pathlib.Path, target: pathlib.Path) -> None:
+        """Push a file or directory to the device.
+
+        The target directory (if the source is a directory), or its
+        parent (if the source if a file) will be created on the device,
+        always.
+
+        :raise DeviceCommandError: If the push command failed.
+        """
+        if source.is_dir():
+            # `adb push` skips empty directories so we have to manually create the
+            # target directory itself.
+            self.adb("shell", "mkdir", "-p", str(target))
+            # `adb push` expects the target to be the host directory not the target
+            # directory itself. So we'll just copy the source directory content instead.
+            sources = [str(child) for child in source.iterdir()]
+            self.adb("push", *sources, str(target))
+        else:
+            self.adb("shell", "mkdir", "-p", str(target.parent))
+            self.adb("push", str(source), str(target))
+
+    def remove(self, target: pathlib.Path, *, recurse: bool = False) -> None:
+        """Remove a file or directory from the device.
+
+        :raise DeviceCommandError: If the remove command failed.
+        """
+        command = ["shell", "rm", "-f"]
+        if recurse:
+            command.append("-r")
+        command.append(str(target))
+        self.adb(*command)
+
 
 def deploy():
     serials = []
@@ -450,7 +482,12 @@
                 disable_privacy_impact_popup(device)
 
             # Push the scenarios, their data, and install the apps
-            prepare_dut(device, "../scenarios", "../scenarios-data", PREBUILTS_PATH)
+            prepare_dut(
+                device,
+                pathlib.Path("../scenarios"),
+                pathlib.Path("../scenarios-data"),
+                PREBUILTS_PATH,
+            )
 
             # Start the viser app
             device.adb(