Merge "Allow changing VM name on configuration" into main
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index bf93226..3b5f9b8 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -24,7 +24,10 @@
     platform_apis: true,
     privileged: true,
     optimize: {
-        enabled: false,
+        // TODO(b/377100096): enable bytecode optimization again
+        //optimize: true,
+        proguard_flags_files: ["proguard.flags"],
+        shrink_resources: true,
     },
     apex_available: [
         "com.android.virt",
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
index c8f5bab..a9102b0 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
@@ -33,6 +33,8 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.lang.ref.WeakReference;
 import java.util.concurrent.ExecutorService;
 
@@ -98,6 +100,7 @@
         super.onDestroy();
     }
 
+    @VisibleForTesting
     public boolean waitForInstallCompleted(long timeoutMillis) {
         return mInstallCompleted.block(timeoutMillis);
     }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
index 5967b6f..82df5c5 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
@@ -25,6 +25,7 @@
 import android.content.pm.ServiceInfo;
 import android.os.Build;
 import android.os.IBinder;
+import android.os.SELinux;
 import android.util.Log;
 
 import androidx.annotation.Nullable;
@@ -37,6 +38,7 @@
 import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
 
 import java.io.BufferedInputStream;
+import java.io.File;
 import java.io.IOException;
 import java.lang.ref.WeakReference;
 import java.net.URL;
@@ -45,6 +47,7 @@
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
 import java.util.Arrays;
+import java.util.Objects;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -60,6 +63,9 @@
                     ? "https://github.com/ikicha/debian_ci/releases/download/release_x86_64/images.tar.gz"
                     : "https://github.com/ikicha/debian_ci/releases/download/release_aarch64/images.tar.gz";
 
+    private static final String SELINUX_FILE_CONTEXT =
+            "u:object_r:virtualizationservice_data_file:";
+
     private final Object mLock = new Object();
 
     private Notification mNotification;
@@ -144,7 +150,9 @@
                 () -> {
                     // TODO(b/374015561): Provide progress update
                     boolean success = downloadFromSdcard() || downloadFromUrl();
-
+                    if (success) {
+                        reLabelImagesSELinuxContext();
+                    }
                     stopForeground(STOP_FOREGROUND_REMOVE);
 
                     synchronized (mLock) {
@@ -156,6 +164,24 @@
                 });
     }
 
+    private void reLabelImagesSELinuxContext() {
+        File payloadFolder = InstallUtils.getInternalStorageDir(this);
+
+        // The context should be u:object_r:privapp_data_file:s0:c35,c257,c512,c768
+        // and we want to get s0:c35,c257,c512,c768 part
+        String level = SELinux.getFileContext(payloadFolder.toString()).split(":", 4)[3];
+        String targetContext = SELINUX_FILE_CONTEXT + level;
+
+        File[] files = payloadFolder.listFiles();
+        for (File file : files) {
+            if (file.isFile() &&
+                    !Objects.equals(SELinux.getFileContext(file.toString()),
+                            targetContext)) {
+                SELinux.setFileContext(file.toString(), targetContext);
+            }
+        }
+    }
+
     private boolean downloadFromSdcard() {
         // Installing from sdcard is preferred, but only supported only in debuggable build.
         if (Build.isDebuggable()) {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index c006e7b..4e32de7 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -52,6 +52,7 @@
 import androidx.activity.result.ActivityResultLauncher;
 import androidx.activity.result.contract.ActivityResultContracts;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.virtualization.vmlauncher.InstallUtils;
 import com.android.virtualization.vmlauncher.VmLauncherService;
 import com.android.virtualization.vmlauncher.VmLauncherServices;
@@ -88,6 +89,7 @@
     private ConditionVariable mBootCompleted = new ConditionVariable();
     private static final int POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE = 101;
     private ActivityResultLauncher<Intent> manageExternalStorageActivityResultLauncher;
+    private static int diskSizeStep;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -103,6 +105,8 @@
         }
 
         setContentView(R.layout.activity_headless);
+        diskSizeStep = getResources().getInteger(
+                R.integer.disk_size_round_up_step_size_in_mb) << 20;
 
         MaterialToolbar toolbar = (MaterialToolbar) findViewById(R.id.toolbar);
         setSupportActionBar(toolbar);
@@ -292,9 +296,9 @@
         }
     }
 
-    private static File getPartitionFile(Context context, String fileName)
+    public static File getPartitionFile(Context context, String fileName)
             throws FileNotFoundException {
-        File file = new File(context.getFilesDir(), fileName);
+        File file = new File(InstallUtils.getInternalStorageDir(context), fileName);
         if (!file.exists()) {
             Log.d(TAG, file.getAbsolutePath() + " - file not found");
             throw new FileNotFoundException("File not found: " + fileName);
@@ -510,19 +514,19 @@
         VmLauncherServices.startVmLauncherService(this, this, notification);
     }
 
+    @VisibleForTesting
     public boolean waitForBootCompleted(long timeoutMillis) {
         return mBootCompleted.block(timeoutMillis);
     }
 
-    private long roundUpDiskSize(long diskSize) {
-        // Round up every disk_size_round_up_step_size_in_mb MB
-        int disk_size_step = getResources().getInteger(
-                R.integer.disk_size_round_up_step_size_in_mb) * 1024 * 1024;
-        return (long) Math.ceil(((double) diskSize) / disk_size_step) * disk_size_step;
+    private static long roundUpDiskSize(long diskSize) {
+        // Round up every diskSizeStep MB
+        return (long) Math.ceil(((double) diskSize) / diskSizeStep) * diskSizeStep;
     }
 
-    private long getMinFilesystemSize(File file) throws IOException, NumberFormatException {
+    public static long getMinFilesystemSize(File file) throws IOException, NumberFormatException {
         try {
+            runE2fsck(file.getAbsolutePath());
             String result = runCommand("/system/bin/resize2fs", "-P", file.getAbsolutePath());
             // The return value is the number of 4k block
             long minSize = Long.parseLong(
@@ -545,12 +549,10 @@
                     getString(R.string.preference_file_key), Context.MODE_PRIVATE);
             SharedPreferences.Editor editor = sharedPref.edit();
 
-            long minDiskSize = getMinFilesystemSize(file);
-            editor.putLong(getString(R.string.preference_min_disk_size_key), minDiskSize);
-
             long currentDiskSize = getFilesystemSize(file);
+            // The default partition size is 6G
             long newSizeInBytes = sharedPref.getLong(getString(R.string.preference_disk_size_key),
-                    roundUpDiskSize(currentDiskSize));
+                    6L << 30);
             editor.putLong(getString(R.string.preference_disk_size_key), newSizeInBytes);
             editor.apply();
 
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsActivity.kt
index dccfea3..03768e9 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsActivity.kt
@@ -20,12 +20,17 @@
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 
+import com.google.android.material.appbar.MaterialToolbar
+
 class SettingsActivity : AppCompatActivity() {
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.settings_activity)
 
+        val toolbar: MaterialToolbar = findViewById(R.id.settings_toolbar)
+        setSupportActionBar(toolbar)
+        supportActionBar?.title = resources.getString(R.string.action_settings)
         val settingsItems = arrayOf(
             SettingsItem(
                 resources.getString(R.string.settings_disk_resize_title),
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
index 54e8ab2..58be98d 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
@@ -55,13 +55,10 @@
                     0
                 )
             ).toFloat();
+        val partition = MainActivity.getPartitionFile(this, "root_part")
         val minDiskSizeMb =
-            bytesToMb(
-                    sharedPref.getLong(
-                    getString(R.string.preference_min_disk_size_key),
-                    0
-                )
-            ).toFloat();
+            bytesToMb(MainActivity.getMinFilesystemSize(partition)).toFloat()
+                .coerceAtMost(diskSizeMb)
 
         val diskSizeText = findViewById<TextView>(R.id.settings_disk_resize_resize_gb_assigned)
         val diskMaxSizeText = findViewById<TextView>(R.id.settings_disk_resize_resize_gb_max)
diff --git a/android/TerminalApp/proguard.flags b/android/TerminalApp/proguard.flags
index b93240c..8433e82 100644
--- a/android/TerminalApp/proguard.flags
+++ b/android/TerminalApp/proguard.flags
@@ -30,4 +30,4 @@
 -keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
 -keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
 
-##---------------End: proguard configuration for Gson  ----------
\ No newline at end of file
+##---------------End: proguard configuration for Gson  ----------
diff --git a/android/TerminalApp/res/layout/settings_activity.xml b/android/TerminalApp/res/layout/settings_activity.xml
index 4fc6229..3ec2617 100644
--- a/android/TerminalApp/res/layout/settings_activity.xml
+++ b/android/TerminalApp/res/layout/settings_activity.xml
@@ -1,10 +1,16 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical"
     android:fitsSystemWindows="true">
 
+    <com.google.android.material.appbar.MaterialToolbar
+        android:id="@+id/settings_toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="?attr/actionBarSize"
+        app:layout_constraintTop_toTopOf="parent"/>
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/settings_list_recycler_view"
         android:layout_marginHorizontal="16dp"
diff --git a/build/debian/build.sh b/build/debian/build.sh
index b4436c1..7fc9035 100755
--- a/build/debian/build.sh
+++ b/build/debian/build.sh
@@ -169,6 +169,19 @@
 	mv "${debian_cloud_image}/image_bookworm_nocloud_${debian_arch}.raw" "${out}"
 }
 
+extract_partitions() {
+	root_partition_num=1
+	efi_partition_num=15
+
+	loop=$(losetup -f --show --partscan image.raw)
+	dd if=${loop}p$root_partition_num of=root_part
+	dd if=${loop}p$efi_partition_num of=efi_part
+	losetup -d ${loop}
+
+	sed -i "s/{root_part_guid}/$(sfdisk --part-uuid image.raw $root_partition_num)/g" vm_config.json
+	sed -i "s/{efi_part_guid}/$(sfdisk --part-uuid image.raw $efi_partition_num)/g" vm_config.json
+}
+
 clean_up() {
 	rm -rf "${workdir}"
 }
@@ -191,18 +204,29 @@
 copy_android_config
 run_fai
 fdisk -l image.raw
-images=(image.raw)
+images=()
+
+cp $(dirname $0)/vm_config.json.${arch} vm_config.json
+
+if [[ "$arch" == "aarch64" ]]; then
+	extract_partitions
+	images+=(
+		root_part
+		efi_part
+	)
+fi
+
 # TODO(b/365955006): remove these lines when uboot supports x86_64 EFI application
 if [[ "$arch" == "x86_64" ]]; then
 	virt-get-kernel -a image.raw
 	mv vmlinuz* vmlinuz
 	mv initrd.img* initrd.img
 	images+=(
+		image.raw
 		vmlinuz
 		initrd.img
 	)
 fi
 
-cp $(dirname $0)/vm_config.json.${arch} vm_config.json
 # --sparse option isn't supported in apache-commons-compress
-tar czv -f images.tar.gz ${images[@]} vm_config.json
\ No newline at end of file
+tar czv -f images.tar.gz ${images[@]} vm_config.json
diff --git a/build/debian/vm_config.json.aarch64 b/build/debian/vm_config.json.aarch64
index 2df0a05..bbe590f 100644
--- a/build/debian/vm_config.json.aarch64
+++ b/build/debian/vm_config.json.aarch64
@@ -2,8 +2,20 @@
     "name": "debian",
     "disks": [
         {
-            "image": "$PAYLOAD_DIR/image.raw",
-            "partitions": [],
+            "partitions": [
+                {
+                    "label": "ROOT",
+                    "path": "$PAYLOAD_DIR/root_part",
+                    "writable": true,
+                    "guid": "{root_part_guid}"
+                },
+                {
+                    "label": "EFI",
+                    "path": "$PAYLOAD_DIR/efi_part",
+                    "writable": false,
+                    "guid": "{efi_part_guid}"
+                }
+            ],
             "writable": true
         }
     ],
diff --git a/guest/pvmfw/Android.bp b/guest/pvmfw/Android.bp
index 4586cca..a5b7494 100644
--- a/guest/pvmfw/Android.bp
+++ b/guest/pvmfw/Android.bp
@@ -16,6 +16,7 @@
         "libcbor_util_nostd",
         "libciborium_nostd",
         "libciborium_io_nostd",
+        "libcoset_nostd",
         "libcstr",
         "libdiced_open_dice_nostd",
         "liblibfdt_nostd",
diff --git a/guest/pvmfw/src/bcc.rs b/guest/pvmfw/src/bcc.rs
index 5317ce9..9260d7f 100644
--- a/guest/pvmfw/src/bcc.rs
+++ b/guest/pvmfw/src/bcc.rs
@@ -21,6 +21,7 @@
 use ciborium::value::Value;
 use core::fmt;
 use core::mem::size_of;
+use coset::{iana, Algorithm, CborSerializable, CoseKey};
 use diced_open_dice::{BccHandover, Cdi, DiceArtifacts, DiceMode};
 use log::trace;
 
@@ -29,16 +30,24 @@
 pub enum BccError {
     CborDecodeError,
     CborEncodeError,
+    CosetError(coset::CoseError),
     DiceError(diced_open_dice::DiceError),
     MalformedBcc(&'static str),
     MissingBcc,
 }
 
+impl From<coset::CoseError> for BccError {
+    fn from(e: coset::CoseError) -> Self {
+        Self::CosetError(e)
+    }
+}
+
 impl fmt::Display for BccError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
             Self::CborDecodeError => write!(f, "Error parsing BCC CBOR"),
             Self::CborEncodeError => write!(f, "Error encoding BCC CBOR"),
+            Self::CosetError(e) => write!(f, "Encountered an error with coset: {e}"),
             Self::DiceError(e) => write!(f, "Dice error: {e:?}"),
             Self::MalformedBcc(s) => {
                 write!(f, "BCC does not have the expected CBOR structure: {s}")
@@ -84,6 +93,7 @@
 /// Represents a (partially) decoded BCC DICE chain.
 pub struct Bcc {
     is_debug_mode: bool,
+    leaf_subject_pubkey: PublicKey,
 }
 
 impl Bcc {
@@ -117,12 +127,18 @@
             .collect::<Result<Vec<_>>>()?;
 
         let is_debug_mode = is_any_payload_debug_mode(&payloads)?;
-        Ok(Self { is_debug_mode })
+        // Safe to unwrap because we checked the length above.
+        let leaf_subject_pubkey = payloads.last().unwrap().subject_public_key()?;
+        Ok(Self { is_debug_mode, leaf_subject_pubkey })
     }
 
     pub fn is_debug_mode(&self) -> bool {
         self.is_debug_mode
     }
+
+    pub fn leaf_subject_pubkey(&self) -> &PublicKey {
+        &self.leaf_subject_pubkey
+    }
 }
 
 fn is_any_payload_debug_mode(payloads: &[BccPayload]) -> Result<bool> {
@@ -144,6 +160,13 @@
 #[repr(transparent)]
 struct BccPayload(Value);
 
+#[derive(Debug, Clone)]
+pub struct PublicKey {
+    /// The COSE key algorithm for the public key, representing the value of the `alg`
+    /// field in the COSE key format of the public key. See RFC 8152, section 7 for details.
+    pub cose_alg: iana::Algorithm,
+}
+
 impl BccEntry {
     pub fn new(entry: Value) -> Self {
         Self(entry)
@@ -178,6 +201,7 @@
 
 const KEY_MODE: i32 = -4670551;
 const MODE_DEBUG: u8 = DiceMode::kDiceModeDebug as u8;
+const SUBJECT_PUBLIC_KEY: i32 = -4670552;
 
 impl BccPayload {
     pub fn is_debug_mode(&self) -> Result<bool> {
@@ -204,6 +228,21 @@
         Ok(mode == MODE_DEBUG.into())
     }
 
+    fn subject_public_key(&self) -> Result<PublicKey> {
+        // BccPayload = {                             ; CWT [RFC8392]
+        // ...
+        //   -4670552 : bstr .cbor PubKeyEd25519 /
+        //              bstr .cbor PubKeyECDSA256 /
+        //              bstr .cbor PubKeyECDSA384,    ; Subject Public Key
+        // ...
+        // }
+        self.value_from_key(SUBJECT_PUBLIC_KEY)
+            .ok_or(BccError::MalformedBcc("Subject public key missing"))?
+            .as_bytes()
+            .ok_or(BccError::MalformedBcc("Subject public key is not a byte string"))
+            .and_then(|v| PublicKey::from_slice(v))
+    }
+
     fn value_from_key(&self, key: i32) -> Option<&Value> {
         // BccPayload is just a map; we only use integral keys, but in general it's legitimate
         // for other things to be present, or for the key we care about not to be present.
@@ -218,3 +257,13 @@
         None
     }
 }
+
+impl PublicKey {
+    fn from_slice(slice: &[u8]) -> Result<Self> {
+        let key = CoseKey::from_slice(slice)?;
+        let Some(Algorithm::Assigned(cose_alg)) = key.alg else {
+            return Err(BccError::MalformedBcc("Invalid algorithm in public key"));
+        };
+        Ok(Self { cose_alg })
+    }
+}
diff --git a/guest/pvmfw/src/main.rs b/guest/pvmfw/src/main.rs
index 1e88c4b..aeced51 100644
--- a/guest/pvmfw/src/main.rs
+++ b/guest/pvmfw/src/main.rs
@@ -200,6 +200,8 @@
         Cow::Owned(truncated_bcc_handover)
     };
 
+    trace!("BCC leaf subject public key algorithm: {:?}", bcc.leaf_subject_pubkey().cose_alg);
+
     dice_inputs
         .write_next_bcc(
             new_bcc_handover.as_ref(),
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java
index f5cc912..57691bf 100644
--- a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java
@@ -20,6 +20,8 @@
 import android.os.FileUtils;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import org.apache.commons.compress.archivers.ArchiveEntry;
 import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
 import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
@@ -64,6 +66,7 @@
         }
     }
 
+    @VisibleForTesting
     public static void deleteInstallation(Context context) {
         FileUtils.deleteContentsAndDir(getInternalStorageDir(context));
     }
diff --git a/tests/Terminal/Android.bp b/tests/Terminal/Android.bp
index cafbe93..029fbea 100644
--- a/tests/Terminal/Android.bp
+++ b/tests/Terminal/Android.bp
@@ -20,5 +20,8 @@
     platform_apis: true,
     test_suites: ["general-tests"],
     instrumentation_for: "VmTerminalApp",
-    data: ["TerminalAppTests-preparer.sh"],
+    data: [
+        "TerminalAppTests-helper.sh",
+        "TerminalAppTests-downloader.sh",
+    ],
 }
diff --git a/tests/Terminal/AndroidTest.xml b/tests/Terminal/AndroidTest.xml
index 262421c..c1f33b9 100644
--- a/tests/Terminal/AndroidTest.xml
+++ b/tests/Terminal/AndroidTest.xml
@@ -26,8 +26,22 @@
         <option name="force-root" value="true"/>
     </target_preparer>
 
+    <!-- Download the VM image and push it to the device -->
     <target_preparer class="com.android.tradefed.targetprep.RunHostScriptTargetPreparer">
-        <option name="script-file" value="TerminalAppTests-preparer.sh" />
+        <option name="script-file" value="TerminalAppTests-downloader.sh" />
+    </target_preparer>
+
+    <!-- Push the helper script to the device -->
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="push" value="TerminalAppTests-helper.sh->/data/local/tmp/helper.sh"/>
+        <option name="post-push" value="chmod 755 /data/local/tmp/helper.sh"/>
+        <option name="cleanup" value="true"/>
+    </target_preparer>
+
+    <!-- Run the helper script to enable/disable the Terminal app before and after the test run -->
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="/data/local/tmp/helper.sh setup"/>
+        <option name="teardown-command" value="/data/local/tmp/helper.sh teardown"/>
     </target_preparer>
 
     <test class="com.android.tradefed.testtype.AndroidJUnitTest">
diff --git a/tests/Terminal/TerminalAppTests-preparer.sh b/tests/Terminal/TerminalAppTests-downloader.sh
similarity index 86%
rename from tests/Terminal/TerminalAppTests-preparer.sh
rename to tests/Terminal/TerminalAppTests-downloader.sh
index 6022d7d..015caad 100644
--- a/tests/Terminal/TerminalAppTests-preparer.sh
+++ b/tests/Terminal/TerminalAppTests-downloader.sh
@@ -19,10 +19,6 @@
 serial=${ANDROID_SERIAL}
 user=$(adb -s ${serial} shell am get-current-user)
 
-# Enable the terminal app
-package_name=$(adb -s ${serial} shell pm list package virtualization.terminal | cut -d ':' -f 2)
-adb -s ${serial} shell pm enable --user ${user} ${package_name}
-
 # Identify file to download
 arch=$(adb -s ${serial} shell getprop ro.bionic.arch)
 if [ ${arch} == "arm64" ]; then
diff --git a/tests/Terminal/TerminalAppTests-helper.sh b/tests/Terminal/TerminalAppTests-helper.sh
new file mode 100644
index 0000000..b9f2b08
--- /dev/null
+++ b/tests/Terminal/TerminalAppTests-helper.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+# Copyright 2024 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -e
+
+user=$(am get-current-user)
+package_name=$(pm list package virtualization.terminal | cut -d ':' -f 2)
+
+if [ $1 == "setup" ]; then
+	pm enable --user ${user} ${package_name}
+elif [ $1 == "teardown" ]; then
+	pm clear --user ${user} ${package_name}
+	pm disable --user ${user} ${package_name}
+	rm -rf /data/media/${user}/linux
+else
+	echo Unsupported command: $1
+	exit 1
+fi