Merge "vmbase: Re-expose fdtpci as vmbase::fdt::pci" into main
diff --git a/OWNERS b/OWNERS
index 717a4db..81217f3 100644
--- a/OWNERS
+++ b/OWNERS
@@ -30,8 +30,7 @@
 victorhsieh@google.com
 
 # Ferrochrome
-per-file android/FerrochromeApp/**=jiyong@google.com,jeongik@google.com
-per-file android/LinuxInstaller/**=jiyong@google.com,jeongik@google.com
 per-file android/TerminalApp/**=jiyong@google.com,jeongik@google.com
 per-file android/VmLauncherApp/**=jiyong@google.com,jeongik@google.com
 per-file libs/vm_launcher_lib/**=jiyong@google.com,jeongik@google.com
+per-file build/debian/**=jiyong@google.com,jeongik@google.com
diff --git a/android/FerrochromeApp/Android.bp b/android/FerrochromeApp/Android.bp
deleted file mode 100644
index 3e4ad14..0000000
--- a/android/FerrochromeApp/Android.bp
+++ /dev/null
@@ -1,42 +0,0 @@
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-java_defaults {
-    name: "VmPayloadInstaller",
-    init_rc: [":custom_vm_setup.rc"],
-    required: ["custom_vm_setup"],
-    // TODO(b/348113995): move this app to product partition
-    system_ext_specific: true,
-    platform_apis: true,
-    privileged: true,
-}
-
-android_app {
-    name: "FerrochromeApp",
-    srcs: ["java/**/*.java"],
-    resource_dirs: ["res"],
-    defaults: ["VmPayloadInstaller"],
-    required: [
-        "privapp-permissions-ferrochrome.xml",
-    ],
-}
-
-prebuilt_etc {
-    name: "privapp-permissions-ferrochrome.xml",
-    src: "privapp-permissions-ferrochrome.xml",
-    sub_dir: "permissions",
-    system_ext_specific: true,
-}
-
-filegroup {
-    name: "custom_vm_setup.rc",
-    srcs: ["custom_vm_setup.rc"],
-}
-
-sh_binary {
-    name: "custom_vm_setup",
-    src: "custom_vm_setup.sh",
-    system_ext_specific: true,
-    host_supported: false,
-}
diff --git a/android/FerrochromeApp/AndroidManifest.xml b/android/FerrochromeApp/AndroidManifest.xml
deleted file mode 100644
index f6d3f6a..0000000
--- a/android/FerrochromeApp/AndroidManifest.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.virtualization.ferrochrome" >
-
-    <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
-    <uses-permission android:name="android.permission.KILL_ALL_BACKGROUND_PROCESSES" />
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER" />
-
-    <queries>
-        <intent>
-            <action android:name="android.virtualization.VM_LAUNCHER" />
-        </intent>
-        <intent>
-            <action android:name="android.virtualization.FERROCHROME_DOWNLOADER" />
-        </intent>
-    </queries>
-    <application
-        android:label="Ferrochrome">
-        <activity android:name=".FerrochromeActivity"
-                  android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
-                  android:screenOrientation="landscape"
-                  android:resizeableActivity="false"
-                  android:exported="true">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-        </activity>
-        <activity android:name=".OpenUrlActivity"
-                  android:theme="@android:style/Theme.NoDisplay"
-                  android:launchMode="singleTask"
-                  android:exported="true">
-            <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="text/*" />
-            </intent-filter>
-        </activity>
-    </application>
-
-</manifest>
diff --git a/android/FerrochromeApp/custom_vm_setup.rc b/android/FerrochromeApp/custom_vm_setup.rc
deleted file mode 100644
index 68f370e..0000000
--- a/android/FerrochromeApp/custom_vm_setup.rc
+++ /dev/null
@@ -1,23 +0,0 @@
-# Copyright (C) 2024 The Android Open Source Project
-#
-# 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.
-
-service custom_vm_setup /system_ext/bin/custom_vm_setup
-    user shell
-    group shell media_rw
-    disabled
-    oneshot
-    seclabel u:r:shell:s0
-
-on property:debug.custom_vm_setup.start=true
-    start custom_vm_setup
diff --git a/android/FerrochromeApp/custom_vm_setup.sh b/android/FerrochromeApp/custom_vm_setup.sh
deleted file mode 100644
index df1a3a6..0000000
--- a/android/FerrochromeApp/custom_vm_setup.sh
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/system/bin/sh
-
-function round_up() {
-  num=$1
-  div=$2
-  echo $((( (( ${num} / ${div} ) + 1) * ${div} )))
-}
-
-function install() {
-  src_dir=$(getprop debug.custom_vm_setup.path)
-  src_dir=${src_dir/#\/storage\/emulated\//\/data\/media\/}
-  dst_dir=/data/local/tmp/
-
-  cat $(find ${src_dir} -name "images.tar.gz*" | sort) | tar xz -C ${dst_dir}
-  cp -u ${src_dir}/vm_config.json ${dst_dir}
-  chmod 666 ${dst_dir}/*
-
-  if [ -f ${dst_dir}state.img ]; then
-    # increase the size of state.img to the multiple of 4096
-    num_blocks=$(du -b -K ${dst_dir}state.img | cut -f 1)
-    required_num_blocks=$(round_up ${num_blocks} 4)
-    additional_blocks=$((( ${required_num_blocks} - ${num_blocks} )))
-    dd if=/dev/zero bs=512 count=${additional_blocks} >> ${dst_dir}state.img
-  fi
-  rm ${src_dir}/images.tar.gz*
-  rm ${src_dir}/vm_config.json
-}
-
-setprop debug.custom_vm_setup.done false
-install
-setprop debug.custom_vm_setup.start false
-setprop debug.custom_vm_setup.done true
diff --git a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
deleted file mode 100644
index dba0078..0000000
--- a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
+++ /dev/null
@@ -1,307 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * 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.
- */
-
-package com.android.virtualization.ferrochrome;
-
-import android.annotation.WorkerThread;
-import android.app.Activity;
-import android.app.ActivityManager;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-import android.os.Environment;
-import android.os.SystemProperties;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.WindowManager;
-import android.widget.TextView;
-
-import java.io.BufferedReader;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-public class FerrochromeActivity extends Activity {
-    private static final String TAG = FerrochromeActivity.class.getName();
-    private static final String ACTION_VM_LAUNCHER = "android.virtualization.VM_LAUNCHER";
-    private static final String ACTION_FERROCHROME_DOWNLOAD =
-            "android.virtualization.FERROCHROME_DOWNLOADER";
-    private static final String EXTRA_FERROCHROME_DEST_DIR = "dest_dir";
-    private static final String EXTRA_FERROCHROME_UPDATE_NEEDED = "update_needed";
-
-    private static final Path DEST_DIR =
-            Path.of(Environment.getExternalStorageDirectory().getPath(), "ferrochrome");
-    private static final String ASSET_DIR = "ferrochrome";
-    private static final Path VERSION_FILE = Path.of(DEST_DIR.toString(), "version");
-
-    private static final int REQUEST_CODE_VMLAUNCHER = 1;
-    private static final int REQUEST_CODE_FERROCHROME_DOWNLOADER = 2;
-
-    private ResolvedActivity mVmLauncher;
-
-    ExecutorService executorService = Executors.newSingleThreadExecutor();
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        if (!isTaskRoot()) {
-            // In case we launched this activity multiple times, only start one instance of this
-            // activity by only starting this as the root activity in task.
-            finish();
-            Log.w(TAG, "Not starting because not task root");
-            return;
-        }
-        setContentView(R.layout.activity_ferrochrome);
-        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-
-        // Find VM Launcher
-        mVmLauncher = ResolvedActivity.resolve(getPackageManager(), ACTION_VM_LAUNCHER);
-        if (mVmLauncher == null) {
-            updateStatus("Failed to resolve VM Launcher");
-            return;
-        }
-
-        // Clean up the existing vm launcher process if there is
-        ActivityManager am = getSystemService(ActivityManager.class);
-        am.killBackgroundProcesses(mVmLauncher.activityInfo.packageName);
-
-        executorService.execute(
-                () -> {
-                    if (hasLocalAssets()) {
-                        if (updateImageIfNeeded()) {
-                            updateStatus("Starting Ferrochrome...");
-                            runOnUiThread(
-                                    () ->
-                                            startActivityForResult(
-                                                    mVmLauncher.intent, REQUEST_CODE_VMLAUNCHER));
-                        }
-                    } else {
-                        tryLaunchDownloader();
-                    }
-                });
-    }
-
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        if (requestCode == REQUEST_CODE_VMLAUNCHER) {
-            finishAndRemoveTask();
-        } else if (requestCode == REQUEST_CODE_FERROCHROME_DOWNLOADER) {
-            String destDir = data.getStringExtra(EXTRA_FERROCHROME_DEST_DIR);
-            boolean updateNeeded =
-                    data.getBooleanExtra(EXTRA_FERROCHROME_UPDATE_NEEDED, /* default= */ true);
-
-            if (resultCode != RESULT_OK || TextUtils.isEmpty(destDir)) {
-                Log.w(
-                        TAG,
-                        "Ferrochrome downloader returned error, code="
-                                + resultCode
-                                + ", dest="
-                                + destDir);
-                updateStatus("User didn't accepted ferrochrome download..");
-                return;
-            }
-
-            Log.w(TAG, "Ferrochrome downloader returned OK");
-
-            if (!updateNeeded) {
-                updateStatus("Starting Ferrochrome...");
-                startActivityForResult(mVmLauncher.intent, REQUEST_CODE_VMLAUNCHER);
-            }
-
-            executorService.execute(
-                    () -> {
-                        if (!extractImages(destDir)) {
-                            updateStatus("Images from downloader looks bad..");
-                            return;
-                        }
-                        updateStatus("Starting Ferrochrome...");
-                        runOnUiThread(
-                                () ->
-                                        startActivityForResult(
-                                                mVmLauncher.intent, REQUEST_CODE_VMLAUNCHER));
-                    });
-        }
-    }
-
-    @WorkerThread
-    private boolean hasLocalAssets() {
-        try {
-            String[] files = getAssets().list(ASSET_DIR);
-            return files != null && files.length > 0;
-        } catch (IOException e) {
-            return false;
-        }
-    }
-
-    @WorkerThread
-    private boolean updateImageIfNeeded() {
-        if (!isUpdateNeeded()) {
-            Log.d(TAG, "No update needed.");
-            return true;
-        }
-
-        try {
-            if (Files.notExists(DEST_DIR)) {
-                Files.createDirectory(DEST_DIR);
-            }
-
-            updateStatus("Copying images...");
-            String[] files = getAssets().list("ferrochrome");
-            for (String file : files) {
-                updateStatus(file);
-                Path dst = Path.of(DEST_DIR.toString(), file);
-                updateFile(getAssets().open("ferrochrome/" + file), dst);
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "Error while updating image: " + e);
-            updateStatus("Failed.");
-            return false;
-        }
-        updateStatus("Done.");
-
-        return extractImages(DEST_DIR.toAbsolutePath().toString());
-    }
-
-    @WorkerThread
-    private void tryLaunchDownloader() {
-        // TODO(jaewan): Add safeguard to check whether ferrochrome downloader is valid.
-        Log.w(TAG, "No built-in assets found. Try again with ferrochrome downloader");
-
-        ResolvedActivity downloader =
-                ResolvedActivity.resolve(getPackageManager(), ACTION_FERROCHROME_DOWNLOAD);
-        if (downloader == null) {
-            Log.d(TAG, "Ferrochrome downloader doesn't exist");
-            updateStatus("ChromeOS image not found. Please go/try-ferrochrome");
-            return;
-        }
-        String pkgName = downloader.activityInfo.packageName;
-        Log.d(TAG, "Resolved Ferrochrome Downloader, pkgName=" + pkgName);
-        updateStatus("Launching Ferrochrome downloader for update");
-
-        // onActivityResult() will handle downloader result.
-        startActivityForResult(downloader.intent, REQUEST_CODE_FERROCHROME_DOWNLOADER);
-    }
-
-    @WorkerThread
-    private boolean extractImages(String destDir) {
-        updateStatus("Extracting images...");
-
-        if (TextUtils.isEmpty(destDir)) {
-            throw new RuntimeException("Internal error: destDir shouldn't be null");
-        }
-
-        SystemProperties.set("debug.custom_vm_setup.path", destDir);
-        SystemProperties.set("debug.custom_vm_setup.done", "false");
-        SystemProperties.set("debug.custom_vm_setup.start", "true");
-        while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
-            try {
-                Thread.sleep(1000);
-            } catch (Exception e) {
-                Log.e(TAG, "Error while extracting image: " + e);
-                updateStatus("Failed.");
-                return false;
-            }
-        }
-
-        updateStatus("Done.");
-        return true;
-    }
-
-    @WorkerThread
-    private boolean isUpdateNeeded() {
-        Path[] pathsToCheck = {DEST_DIR, VERSION_FILE};
-        for (Path p : pathsToCheck) {
-            if (Files.notExists(p)) {
-                Log.d(TAG, p.toString() + " does not exist.");
-                return true;
-            }
-        }
-
-        try {
-            String installedVer = readLine(new FileInputStream(VERSION_FILE.toFile()));
-            String updatedVer = readLine(getAssets().open("ferrochrome/version"));
-            if (installedVer.equals(updatedVer)) {
-                return false;
-            }
-            Log.d(TAG, "Version mismatch. Installed: " + installedVer + "  Updated: " + updatedVer);
-        } catch (IOException e) {
-            Log.e(TAG, "Error while checking version: " + e);
-        }
-        return true;
-    }
-
-    private static String readLine(InputStream input) throws IOException {
-        try (BufferedReader reader = new BufferedReader(new InputStreamReader(input))) {
-            return reader.readLine();
-        } catch (IOException e) {
-            throw e;
-        }
-    }
-
-    private static void updateFile(InputStream input, Path path) throws IOException {
-        try {
-            Files.copy(input, path, StandardCopyOption.REPLACE_EXISTING);
-        } finally {
-            input.close();
-        }
-    }
-
-    private void updateStatus(String line) {
-        runOnUiThread(
-                () -> {
-                    TextView statusView = findViewById(R.id.status_txt_view);
-                    statusView.append(line + "\n");
-                });
-    }
-
-    private static final class ResolvedActivity {
-        public final ActivityInfo activityInfo;
-        public final Intent intent;
-
-        private ResolvedActivity(ActivityInfo activityInfo, Intent intent) {
-            this.activityInfo = activityInfo;
-            this.intent = intent;
-        }
-
-        /* synthetic access */
-        static ResolvedActivity resolve(PackageManager pm, String action) {
-            Intent intent = new Intent(action).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
-            List<ResolveInfo> resolveInfos =
-                    pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
-            if (resolveInfos == null || resolveInfos.size() != 1) {
-                Log.w(
-                        TAG,
-                        "Failed to resolve activity, action="
-                                + action
-                                + ", resolved="
-                                + resolveInfos);
-                return null;
-            }
-            ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
-            intent.setClassName(activityInfo.packageName, activityInfo.name);
-            return new ResolvedActivity(activityInfo, intent);
-        }
-    }
-}
diff --git a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
deleted file mode 100644
index 433e89c..0000000
--- a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * 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.
- */
-
-package com.android.virtualization.ferrochrome;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.util.Log;
-
-public class OpenUrlActivity extends Activity {
-    private static final String TAG = OpenUrlActivity.class.getSimpleName();
-
-    private static final String ACTION_VM_OPEN_URL = "android.virtualization.VM_OPEN_URL";
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        finish();
-
-        if (!Intent.ACTION_SEND.equals(getIntent().getAction())) {
-            return;
-        }
-        String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
-        if (text == null) {
-            return;
-        }
-        Uri uri = Uri.parse(text);
-        if (uri == null) {
-            return;
-        }
-        String scheme = uri.getScheme();
-        if (!("http".equals(scheme) || "https".equals(scheme) || "mailto".equals(scheme))) {
-            Log.e(TAG, "Unsupported URL scheme: " + scheme);
-            return;
-        }
-        Log.i(TAG, "Sending " + scheme + " URL to VM");
-        startActivity(
-                new Intent(ACTION_VM_OPEN_URL)
-                        .setFlags(
-                                Intent.FLAG_ACTIVITY_SINGLE_TOP
-                                        | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP)
-                        .putExtra(Intent.EXTRA_TEXT, text));
-    }
-}
diff --git a/android/FerrochromeApp/privapp-permissions-ferrochrome.xml b/android/FerrochromeApp/privapp-permissions-ferrochrome.xml
deleted file mode 100644
index 987db9c..0000000
--- a/android/FerrochromeApp/privapp-permissions-ferrochrome.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<permissions>
-    <privapp-permissions package="com.android.virtualization.ferrochrome">
-        <permission name="android.permission.KILL_ALL_BACKGROUND_PROCESSES"/>
-    </privapp-permissions>
-</permissions>
\ No newline at end of file
diff --git a/android/FerrochromeApp/repack.sh b/android/FerrochromeApp/repack.sh
deleted file mode 100755
index b2a96dd..0000000
--- a/android/FerrochromeApp/repack.sh
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/bin/bash
-# Repacks chromiumos_*.bin into the assets of FerrochromeApp
-
-usage() {
-	echo "Usage: $0 CHROME_OS_DISK_IMAGE"
-	exit 1
-}
-
-if [ "$#" -ne 1 ]; then
-	usage
-fi
-
-disk=$1
-
-loop=$(sudo losetup --show -f -P ${disk})
-kern=$(sudo fdisk -x ${loop} | grep KERN-A | awk "{print\$1}")
-root=$(sudo fdisk -x ${loop} | grep ROOT-A | awk "{print\$1}")
-efi=$(sudo fdisk -x ${loop} | grep EFI-SYSTEM | awk "{print\$1}")
-state=$(sudo fdisk -x ${loop} | grep STATE | awk "{print\$1}")
-root_guid=$(sudo fdisk -x ${loop} | grep ROOT-A | awk "{print\$6}")
-
-tempdir=$(mktemp -d)
-pushd ${tempdir} > /dev/null
-echo Extracting partition images...
-sudo cp --sparse=always ${kern} kernel.img
-sudo cp --sparse=always ${root} root.img
-sudo cp --sparse=always ${efi} efi.img
-sudo cp --sparse=always ${state} state.img
-sudo chmod 777 *.img
-
-echo Archiving. This can take long...
-tar czvS -f images.tar.gz *.img
-
-echo Splitting...
-split -b 100M -d images.tar.gz images.tar.gz.part
-
-popd > /dev/null
-asset_dir=$(dirname $0)/assets/ferrochrome
-echo Updating ${asset_dir}...
-vm_config_template=$(dirname $0)/vm_config.json.template
-mkdir -p ${asset_dir}
-rm ${asset_dir}/images.tar.gz.part*
-mv ${tempdir}/images.tar.gz.part* ${asset_dir}
-sed -E s/GUID/${root_guid}/ ${vm_config_template} > ${asset_dir}/vm_config.json
-
-echo Calculating hash...
-hash=$(cat ${tempdir}/images.tar.gz ${asset_dir}/vm_config.json | sha1sum | cut -d' ' -f 1)
-echo ${hash} > ${asset_dir}/version
-
-echo Cleanup...
-sudo losetup -d ${loop}
-rm -rf ${tempdir}
-echo Done.
diff --git a/android/FerrochromeApp/vm_config.json.template b/android/FerrochromeApp/vm_config.json.template
deleted file mode 100644
index 380f016..0000000
--- a/android/FerrochromeApp/vm_config.json.template
+++ /dev/null
@@ -1,56 +0,0 @@
-{
-    "name": "cros",
-    "disks": [
-        {
-            "writable": true,
-            "partitions": [
-                {
-                    "label": "STATE",
-                    "path": "/data/local/tmp/state.img",
-                    "writable": true
-                },
-                {
-                    "label": "KERN-A",
-                    "path": "/data/local/tmp/kernel.img"
-                },
-                {
-                    "label": "ROOT-A",
-                    "path": "/data/local/tmp/root.img",
-                    "guid": "GUID"
-                },
-                {
-                    "label": "EFI-SYSTEM",
-                    "path": "/data/local/tmp/efi.img"
-                }
-            ]
-        }
-    ],
-    "protected": false,
-    "cpu_topology": "match_host",
-    "platform_version": "~1.0",
-    "memory_mib": 8096,
-    "debuggable": true,
-    "console_out": true,
-    "connect_console": true,
-    "console_input_device": "hvc0",
-    "network": true,
-    "input": {
-        "touchscreen": true,
-        "keyboard": true,
-        "mouse": true,
-        "trackpad": true,
-        "switches": true
-    },
-    "audio": {
-        "speaker": true,
-        "microphone": true
-    },
-    "gpu": {
-        "backend": "virglrenderer",
-        "context_types": ["virgl2"]
-    },
-    "display": {
-        "scale": "0.77",
-        "refresh_rate": "30"
-    }
-}
diff --git a/android/LinuxInstaller/Android.bp b/android/LinuxInstaller/Android.bp
deleted file mode 100644
index f70452d..0000000
--- a/android/LinuxInstaller/Android.bp
+++ /dev/null
@@ -1,41 +0,0 @@
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_app {
-    name: "LinuxInstallerApp",
-    srcs: ["java/**/*.java"],
-    resource_dirs: ["res"],
-    asset_dirs: ["assets"],
-    manifest: "AndroidManifest.xml",
-    defaults: ["VmPayloadInstaller"],
-    overrides: ["LinuxInstallerAppStub"],
-    required: [
-        "privapp-permissions-linuxinstaller.xml",
-    ],
-    certificate: ":com.android.virtualization.linuxinstaller_certificate",
-}
-
-android_app {
-    name: "LinuxInstallerAppStub",
-    srcs: ["java/**/*.java"],
-    resource_dirs: ["res"],
-    manifest: "AndroidManifest_stub.xml",
-    defaults: ["VmPayloadInstaller"],
-    required: [
-        "privapp-permissions-linuxinstaller.xml",
-    ],
-    certificate: ":com.android.virtualization.linuxinstaller_certificate",
-}
-
-prebuilt_etc {
-    name: "privapp-permissions-linuxinstaller.xml",
-    src: "privapp-permissions-linuxinstaller.xml",
-    sub_dir: "permissions",
-    system_ext_specific: true,
-}
-
-android_app_certificate {
-    name: "com.android.virtualization.linuxinstaller_certificate",
-    certificate: "com_android_virtualization_linuxinstaller",
-}
diff --git a/android/LinuxInstaller/AndroidManifest.xml b/android/LinuxInstaller/AndroidManifest.xml
deleted file mode 100644
index e5653f6..0000000
--- a/android/LinuxInstaller/AndroidManifest.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.virtualization.linuxinstaller"
-    android:versionCode="2100000000" >
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />
-    <queries>
-        <intent>
-            <action android:name="android.virtualization.VM_TERMINAL" />
-        </intent>
-    </queries>
-    <application
-        android:label="LinuxInstaller">
-        <activity android:name=".MainActivity"
-                  android:exported="true">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-        </activity>
-    </application>
-
-</manifest>
diff --git a/android/LinuxInstaller/AndroidManifest_stub.xml b/android/LinuxInstaller/AndroidManifest_stub.xml
deleted file mode 100644
index 49365ea..0000000
--- a/android/LinuxInstaller/AndroidManifest_stub.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.virtualization.linuxinstaller" >
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />
-    <queries>
-        <intent>
-            <action android:name="android.virtualization.VM_TERMINAL" />
-        </intent>
-    </queries>
-    <application
-        android:label="LinuxInstaller">
-        <activity android:name=".MainActivity"
-                  android:exported="true">
-        </activity>
-    </application>
-
-</manifest>
diff --git a/android/LinuxInstaller/com_android_virtualization_linuxinstaller.pk8 b/android/LinuxInstaller/com_android_virtualization_linuxinstaller.pk8
deleted file mode 100644
index 3f74303..0000000
--- a/android/LinuxInstaller/com_android_virtualization_linuxinstaller.pk8
+++ /dev/null
Binary files differ
diff --git a/android/LinuxInstaller/com_android_virtualization_linuxinstaller.x509.pem b/android/LinuxInstaller/com_android_virtualization_linuxinstaller.x509.pem
deleted file mode 100644
index 3ca64b7..0000000
--- a/android/LinuxInstaller/com_android_virtualization_linuxinstaller.x509.pem
+++ /dev/null
@@ -1,24 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIEETCCAvmgAwIBAgIUfBxyELS+ri3QErq8DXHu+47xx4EwDQYJKoZIhvcNAQEL
-BQAwgZYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
-DA1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwH
-QW5kcm9pZDEyMDAGA1UEAwwpY29tX2FuZHJvaWRfdmlydHVhbGl6YXRpb25fbGlu
-dXhpbnN0YWxsZXIwIBcNMjQwODMwMTIyNjU2WhgPMjA1MjAxMTYxMjI2NTZaMIGW
-MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
-bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv
-aWQxMjAwBgNVBAMMKWNvbV9hbmRyb2lkX3ZpcnR1YWxpemF0aW9uX2xpbnV4aW5z
-dGFsbGVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8V/rH9ju6Wce
-1BdWuxfWaLmZJHGShXeDO6MB86Wrm10m26j9PFzd8/8FRKsZaujZphwNZsqBsdlt
-pWeNKts9T9luZn19Ci4E8A2EtgSxmfI8Fjwj/OJHHO0hG5+JcwIlUnmFQPcGtu/r
-EL3i7SfcF2ok+IC6aKYohnSbo+YkjyCSwb39i6POe6v6cPIZJtmOnecThS+fYCYR
-2yoMSSr3Bf8ayySrG0pJp7xZ1I5NixK6hUFZhQRLusyiv/KYTpAElMd+n1YJEYbf
-pW30DYAu+31S0hx8JXncFmI0uG3Zxx+LgNQwY8OPV6NPFfVwMPluZR6ep0tZ6q7e
-KIV2w5uC7QIDAQABo1MwUTAdBgNVHQ4EFgQU6FBYv7mW+9DR9q0c9uS4NNdX4Acw
-HwYDVR0jBBgwFoAU6FBYv7mW+9DR9q0c9uS4NNdX4AcwDwYDVR0TAQH/BAUwAwEB
-/zANBgkqhkiG9w0BAQsFAAOCAQEAj3bvUpwKjvpCggXzjMNkn7fAaQ0s1BubnkFe
-ge4zwz4tObP3OGRcxt5V9R5EZ7UY6bPcybA/rfg9FCzjcUQOBjmuepcQpbNHFW2I
-lasFa42UHkHSUFzeg2n9UC5iO3B+sclOr4EPaEE4HbG4B2vj++BYMW3C7PDyHc7R
-fq5ZsEEWcYUa8qZCO46I8AbMZ8iv1HpR4mZeQMkSxhD3uVHDQW+VqDTpzne/YBkJ
-yNfjpgFVZ/Y1E6BvvjzWZpBfj668fo7P3DekWHbvPPr/DiZ7OA6PCmAH1FBsi2c+
-xPgb9clDc2Zjb2Cd9lAoZdeB14zDOh6ZCF1c/i+qYt5tA9t+GA==
------END CERTIFICATE-----
diff --git a/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java b/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java
deleted file mode 100644
index 1d875cb..0000000
--- a/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * 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.
- */
-
-package com.android.virtualization.linuxinstaller;
-
-import android.annotation.WorkerThread;
-import android.app.Activity;
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-import android.os.Environment;
-import android.os.SystemProperties;
-import android.text.TextUtils;
-import android.util.Log;
-import android.widget.TextView;
-
-import libcore.io.Streams;
-
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-public class MainActivity extends Activity {
-    private static final String TAG = "LinuxInstaller";
-    private static final String ACTION_VM_TERMINAL = "android.virtualization.VM_TERMINAL";
-
-    private static final Path DEST_DIR =
-            Path.of(Environment.getExternalStorageDirectory().getPath(), "linux");
-
-    private static final String ASSET_DIR = "linux";
-    private static final String HASH_FILE_NAME = "hash";
-    private static final Path HASH_FILE = Path.of(DEST_DIR.toString(), HASH_FILE_NAME);
-
-    ExecutorService executorService = Executors.newSingleThreadExecutor();
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_main);
-
-        executorService.execute(this::installLinuxImage);
-    }
-
-    private void installLinuxImage() {
-        ComponentName vmTerminalComponent = resolve(getPackageManager(), ACTION_VM_TERMINAL);
-        if (vmTerminalComponent == null) {
-            updateStatus("Failed to resolve VM terminal");
-            return;
-        }
-
-        if (!hasLocalAssets()) {
-            updateStatus("No local assets");
-            return;
-        }
-        try {
-            updateImageIfNeeded();
-        } catch (IOException e) {
-            Log.e(TAG, "failed to update image", e);
-            return;
-        }
-        updateStatus("Enabling terminal app...");
-        getPackageManager()
-                .setComponentEnabledSetting(
-                        vmTerminalComponent,
-                        PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
-                        PackageManager.DONT_KILL_APP);
-        updateStatus("Done.");
-    }
-
-    @WorkerThread
-    private boolean hasLocalAssets() {
-        try {
-            String[] files = getAssets().list(ASSET_DIR);
-            return files != null && files.length > 0;
-        } catch (IOException e) {
-            Log.e(TAG, "there is an error during listing up assets", e);
-            return false;
-        }
-    }
-
-    @WorkerThread
-    private void updateImageIfNeeded() throws IOException {
-        if (!isUpdateNeeded()) {
-            Log.d(TAG, "No update needed.");
-            return;
-        }
-
-        try {
-            if (Files.notExists(DEST_DIR)) {
-                Files.createDirectory(DEST_DIR);
-            }
-
-            updateStatus("Copying images...");
-            String[] files = getAssets().list(ASSET_DIR);
-            for (String file : files) {
-                updateStatus(file);
-                Path dst = Path.of(DEST_DIR.toString(), file);
-                updateFile(getAssets().open(ASSET_DIR + "/" + file), dst);
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "Error while updating image: " + e);
-            updateStatus("Failed to update image.");
-            throw e;
-        }
-        extractImages(DEST_DIR.toAbsolutePath().toString());
-    }
-
-    @WorkerThread
-    private void extractImages(String destDir) throws IOException {
-        updateStatus("Extracting images...");
-
-        if (TextUtils.isEmpty(destDir)) {
-            throw new RuntimeException("Internal error: destDir shouldn't be null");
-        }
-
-        SystemProperties.set("debug.custom_vm_setup.path", destDir);
-        SystemProperties.set("debug.custom_vm_setup.done", "false");
-        SystemProperties.set("debug.custom_vm_setup.start", "true");
-        while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
-            try {
-                Thread.sleep(1000);
-            } catch (InterruptedException e) {
-                Log.e(TAG, "Error while extracting image: " + e);
-                updateStatus("Failed to extract image.");
-                throw new IOException("extracting image is interrupted", e);
-            }
-        }
-    }
-
-    @WorkerThread
-    private boolean isUpdateNeeded() {
-        Path[] pathsToCheck = {DEST_DIR, HASH_FILE};
-        for (Path p : pathsToCheck) {
-            if (Files.notExists(p)) {
-                Log.d(TAG, p.toString() + " does not exist.");
-                return true;
-            }
-        }
-
-        try {
-            String installedHash = readAll(new FileInputStream(HASH_FILE.toFile()));
-            String updatedHash = readAll(getAssets().open(ASSET_DIR + "/" + HASH_FILE_NAME));
-            if (installedHash.equals(updatedHash)) {
-                return false;
-            }
-            Log.d(TAG, "Hash mismatch. Installed: " + installedHash + "  Updated: " + updatedHash);
-        } catch (IOException e) {
-            Log.e(TAG, "Error while checking hash: " + e);
-        }
-        return true;
-    }
-
-    private static String readAll(InputStream input) throws IOException {
-        return Streams.readFully(new InputStreamReader(input)).strip();
-    }
-
-    private static void updateFile(InputStream input, Path path) throws IOException {
-        try (input) {
-            Files.copy(input, path, StandardCopyOption.REPLACE_EXISTING);
-        }
-    }
-
-    private void updateStatus(String line) {
-        runOnUiThread(
-                () -> {
-                    TextView statusView = findViewById(R.id.status_txt_view);
-                    statusView.append(line + "\n");
-                });
-    }
-
-    private ComponentName resolve(PackageManager pm, String action) {
-        Intent intent = new Intent(action);
-        List<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, PackageManager.MATCH_ALL);
-        if (resolveInfos.size() != 1) {
-            Log.w(
-                    TAG,
-                    "Failed to resolve activity, action=" + action + ", resolved=" + resolveInfos);
-            return null;
-        }
-        ActivityInfo activityInfo = resolveInfos.getFirst().activityInfo;
-        // MainActivityAlias shows in Launcher
-        return new ComponentName(activityInfo.packageName, activityInfo.name + "Alias");
-    }
-}
diff --git a/android/LinuxInstaller/linux_image_builder/commands b/android/LinuxInstaller/linux_image_builder/commands
deleted file mode 100644
index 4d27475..0000000
--- a/android/LinuxInstaller/linux_image_builder/commands
+++ /dev/null
@@ -1,11 +0,0 @@
-upload init.sh:/root
-upload vsock.py:/usr/local/bin
-upload /tmp/ttyd:/usr/local/bin
-upload ttyd.service:/etc/systemd/system
-upload vsockip.service:/etc/systemd/system
-chmod 0777:/root/init.sh
-firstboot-command "/root/init.sh"
-chmod 0644:/etc/systemd/system/vsockip.service
-chmod 0644:/etc/systemd/system/ttyd.service
-chmod 0777:/usr/local/bin/vsock.py
-chmod 0777:/usr/local/bin/ttyd
diff --git a/android/LinuxInstaller/linux_image_builder/init.sh b/android/LinuxInstaller/linux_image_builder/init.sh
deleted file mode 100644
index bec5ac5..0000000
--- a/android/LinuxInstaller/linux_image_builder/init.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-systemctl daemon-reload
-systemctl start ttyd && sudo systemctl enable ttyd
-systemctl start vsockip && sudo systemctl enable vsockip
diff --git a/android/LinuxInstaller/linux_image_builder/setup.sh b/android/LinuxInstaller/linux_image_builder/setup.sh
deleted file mode 100755
index 2883e61..0000000
--- a/android/LinuxInstaller/linux_image_builder/setup.sh
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/bin/bash
-
-pushd $(dirname $0) > /dev/null
-tempdir=$(mktemp -d)
-echo Get Debian image and dependencies...
-wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-nocloud-arm64.raw -O ${tempdir}/debian.img
-wget https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.aarch64 -O ${tempdir}/ttyd
-
-echo Customize the image...
-virt-customize --commands-from-file <(sed "s|/tmp|$tempdir|g" commands) -a ${tempdir}/debian.img
-
-asset_dir=../assets/linux
-mkdir -p ${asset_dir}
-
-echo Copy files...
-
-pushd ${tempdir} > /dev/null
-tar czvS -f images.tar.gz debian.img
-popd > /dev/null
-mv ${tempdir}/images.tar.gz ${asset_dir}/images.tar.gz
-cp vm_config.json ${asset_dir}
-
-echo Calculating hash...
-hash=$(cat ${asset_dir}/images.tar.gz ${asset_dir}/vm_config.json | sha1sum | cut -d' ' -f 1)
-echo ${hash} > ${asset_dir}/hash
-
-popd > /dev/null
-echo Cleaning up...
-rm -rf ${tempdir}
\ No newline at end of file
diff --git a/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh b/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh
deleted file mode 100755
index c543b2a..0000000
--- a/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/bin/bash
-
-pushd $(dirname $0) > /dev/null
-tempdir=$(mktemp -d)
-echo Get Debian image and dependencies...
-wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-nocloud-amd64.raw -O ${tempdir}/debian.img
-wget https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.x86_64 -O ${tempdir}/ttyd
-
-echo Customize the image...
-virt-customize --commands-from-file <(sed "s|/tmp|$tempdir|g" commands) -a ${tempdir}/debian.img
-
-asset_dir=../assets/linux
-mkdir -p ${asset_dir}
-
-echo Copy files...
-
-pushd ${tempdir} > /dev/null
-tar czvS -f images.tar.gz debian.img
-popd > /dev/null
-mv ${tempdir}/images.tar.gz ${asset_dir}/images.tar.gz
-cp vm_config.json ${asset_dir}
-
-echo Calculating hash...
-hash=$(cat ${asset_dir}/images.tar.gz ${asset_dir}/vm_config.json | sha1sum | cut -d' ' -f 1)
-echo ${hash} > ${asset_dir}/hash
-
-popd > /dev/null
-echo Cleaning up...
-rm -rf ${tempdir}
\ No newline at end of file
diff --git a/android/LinuxInstaller/linux_image_builder/ttyd.service b/android/LinuxInstaller/linux_image_builder/ttyd.service
deleted file mode 100644
index f71557d..0000000
--- a/android/LinuxInstaller/linux_image_builder/ttyd.service
+++ /dev/null
@@ -1,12 +0,0 @@
-[Unit]
-Description=TTYD
-After=syslog.target
-After=network.target
-[Service]
-ExecStart=/usr/local/bin/ttyd -W screen -aAxR -S main login
-Type=simple
-Restart=always
-User=root
-Group=root
-[Install]
-WantedBy=multi-user.target
diff --git a/android/LinuxInstaller/linux_image_builder/vsock.py b/android/LinuxInstaller/linux_image_builder/vsock.py
deleted file mode 100644
index 292d953..0000000
--- a/android/LinuxInstaller/linux_image_builder/vsock.py
+++ /dev/null
@@ -1,57 +0,0 @@
-#!/usr/bin/env python3
-
-import socket
-
-# Constants for vsock (from linux/vm_sockets.h)
-AF_VSOCK = 40
-SOCK_STREAM = 1
-VMADDR_CID_ANY = -1
-
-def get_local_ip():
-    """Retrieves the first IPv4 address found on the system.
-
-    Returns:
-        str: The local IPv4 address, or '127.0.0.1' if no IPv4 address is found.
-    """
-
-    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-    try:
-        s.connect(('8.8.8.8', 80))
-        ip = s.getsockname()[0]
-    except Exception:
-        ip = '127.0.0.1'
-    finally:
-        s.close()
-    return ip
-
-def main():
-    PORT = 1024
-
-    # Create a vsock socket
-    server_socket = socket.socket(AF_VSOCK, SOCK_STREAM)
-
-    # Bind the socket to the server address
-    server_address = (VMADDR_CID_ANY, PORT)
-    server_socket.bind(server_address)
-
-    # Listen for incoming connections
-    server_socket.listen(1)
-    print(f"VSOCK server listening on port {PORT}...")
-
-    while True:
-        # Accept a connection
-        connection, client_address = server_socket.accept()
-        print(f"Connection from: {client_address}")
-
-        try:
-            # Get the local IP address
-            local_ip = get_local_ip()
-
-            # Send the IP address to the client
-            connection.sendall(local_ip.encode())
-        finally:
-            # Close the connection
-            connection.close()
-
-if __name__ == "__main__":
-    main()
diff --git a/android/LinuxInstaller/linux_image_builder/vsockip.service b/android/LinuxInstaller/linux_image_builder/vsockip.service
deleted file mode 100644
index a29020b..0000000
--- a/android/LinuxInstaller/linux_image_builder/vsockip.service
+++ /dev/null
@@ -1,12 +0,0 @@
-[Unit]
-Description=vsock ip service
-After=syslog.target
-After=network.target
-[Service]
-ExecStart=/usr/bin/python3 /usr/local/bin/vsock.py
-Type=simple
-Restart=always
-User=root
-Group=root
-[Install]
-WantedBy=multi-user.target
diff --git a/android/LinuxInstaller/privapp-permissions-linuxinstaller.xml b/android/LinuxInstaller/privapp-permissions-linuxinstaller.xml
deleted file mode 100644
index e46ec97..0000000
--- a/android/LinuxInstaller/privapp-permissions-linuxinstaller.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<permissions>
-    <privapp-permissions package="com.android.virtualization.linuxinstaller">
-        <permission name="android.permission.CHANGE_COMPONENT_ENABLED_STATE"/>
-    </privapp-permissions>
-</permissions>
\ No newline at end of file
diff --git a/android/LinuxInstaller/res/layout/activity_main.xml b/android/LinuxInstaller/res/layout/activity_main.xml
deleted file mode 100644
index 3967167..0000000
--- a/android/LinuxInstaller/res/layout/activity_main.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:fitsSystemWindows="true"
-    android:paddingLeft="16dp"
-    android:paddingRight="16dp">
-  <TextView
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:textSize="28sp"
-      android:id="@+id/status_txt_view"/>
-
-</RelativeLayout>
diff --git a/android/LinuxInstaller/.gitignore b/android/TerminalApp/.gitignore
similarity index 100%
rename from android/LinuxInstaller/.gitignore
rename to android/TerminalApp/.gitignore
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index 1a7c581..932ca76 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -4,10 +4,17 @@
 
 android_app {
     name: "VmTerminalApp",
-    srcs: ["java/**/*.java"],
+    srcs: [
+        "java/**/*.java",
+        "java/**/*.kt",
+    ],
     resource_dirs: ["res"],
+    asset_dirs: ["assets"],
     static_libs: [
         "vm_launcher_lib",
+        "androidx-constraintlayout_constraintlayout",
+        "com.google.android.material_material",
+        "androidx.window_window",
     ],
     platform_apis: true,
     privileged: true,
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index e338c49..28b5436 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -1,40 +1,63 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.virtualization.terminal" >
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.android.virtualization.terminal">
 
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
     <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
+    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
 
     <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
+
     <application
-	android:label="@string/app_name"
+        android:label="@string/app_name"
         android:icon="@mipmap/ic_launcher"
-        android:usesCleartextTraffic="true">
+        android:theme="@style/Theme.Material3.DayNight.NoActionBar"
+        android:usesCleartextTraffic="true"
+        android:enabled="false">
         <activity android:name=".MainActivity"
                   android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode|screenLayout|smallestScreenSize"
                   android:exported="true">
             <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
                 <action android:name="android.virtualization.VM_TERMINAL" />
+                <category android:name="android.intent.category.LAUNCHER" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-        <activity-alias
-            android:name=".MainActivityAlias"
-            android:targetActivity="com.android.virtualization.terminal.MainActivity"
-            android:exported="true"
-            android:enabled="false" >
+        <activity android:name=".SettingsActivity">
+        </activity>
+        <activity android:name=".SettingsDiskResizeActivity">
+        </activity>
+        <activity android:name=".SettingsPortForwardingActivity">
+        </activity>
+        <activity android:name=".SettingsRecoveryActivity">
+        </activity>
+        <property
+            android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
+            android:value="true" />
+        <provider
+            android:name="androidx.startup.InitializationProvider"
+            android:authorities="${applicationId}.androidx-startup"
+            android:exported="false"
+            tools:node="merge">
+            <meta-data
+                android:name="${applicationId}.SplitInitializer"
+                android:value="androidx.startup" />
+        </provider>
+        <activity android:name=".InstallerActivity"
+            android:exported="false">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
-        </activity-alias>
+        </activity>
 
         <service
             android:name="com.android.virtualization.vmlauncher.VmLauncherService"
-            android:enabled="true"
             android:exported="false"
             android:foregroundServiceType="specialUse">
             <property
diff --git a/android/LinuxInstaller/assets/.gitkeep b/android/TerminalApp/assets/.gitkeep
similarity index 100%
rename from android/LinuxInstaller/assets/.gitkeep
rename to android/TerminalApp/assets/.gitkeep
diff --git a/android/TerminalApp/assets/client.p12 b/android/TerminalApp/assets/client.p12
new file mode 100644
index 0000000..f1f5820
--- /dev/null
+++ b/android/TerminalApp/assets/client.p12
Binary files differ
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
new file mode 100644
index 0000000..a49ea72
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.virtualization.terminal;
+
+import android.app.Activity;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.TextView;
+
+import com.android.virtualization.vmlauncher.InstallUtils;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class InstallerActivity extends Activity {
+    private static final String TAG = "LinuxInstaller";
+
+    ExecutorService executorService = Executors.newSingleThreadExecutor();
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setResult(RESULT_CANCELED);
+
+        setContentView(R.layout.activity_installer);
+
+        executorService.execute(this::installLinuxImage);
+    }
+
+    private void installLinuxImage() {
+        Log.d(TAG, "installLinuxImage");
+        // Installing from sdcard is supported only in debuggable build.
+        if (Build.isDebuggable()) {
+            updateStatus("try /sdcard/linux/images.tar.gz");
+            if (InstallUtils.installImageFromExternalStorage(this)) {
+                Log.d(TAG, "success / sdcard");
+                updateStatus("image is installed from /sdcard/linux/images.tar.gz");
+                setResult(RESULT_OK);
+                finish();
+                return;
+            }
+            Log.d(TAG, "fail / sdcard");
+            updateStatus("There is no /sdcard/linux/images.tar.gz");
+        }
+        setResult(RESULT_CANCELED, null);
+        finish();
+    }
+
+    private void updateStatus(String line) {
+        runOnUiThread(
+                () -> {
+                    TextView statusView = findViewById(R.id.status_txt_view);
+                    statusView.append(line + "\n");
+                });
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index a6723fb..d71a17c 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -15,83 +15,356 @@
  */
 package com.android.virtualization.terminal;
 
-import android.app.Activity;
-import android.content.ClipData;
-import android.content.ClipboardManager;
+import android.Manifest;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Icon;
+import android.net.http.SslError;
+import android.os.Build;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
+import android.system.ErrnoException;
+import android.system.Os;
 import android.util.Log;
 import android.view.Menu;
 import android.view.MenuItem;
+import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+import android.webkit.ClientCertRequest;
+import android.webkit.SslErrorHandler;
 import android.webkit.WebChromeClient;
+import android.webkit.WebResourceError;
+import android.webkit.WebResourceRequest;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
-import android.widget.TextView;
 import android.widget.Toast;
 
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.android.virtualization.vmlauncher.InstallUtils;
 import com.android.virtualization.vmlauncher.VmLauncherServices;
 
-public class MainActivity extends Activity implements VmLauncherServices.VmLauncherServiceCallback {
+import com.google.android.material.appbar.MaterialToolbar;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+
+public class MainActivity extends AppCompatActivity
+        implements VmLauncherServices.VmLauncherServiceCallback,
+                AccessibilityManager.TouchExplorationStateChangeListener {
+
     private static final String TAG = "VmTerminalApp";
-    private String mVmIpAddr;
+    private static final String VM_ADDR = "192.168.0.2";
+    private static final int TTYD_PORT = 7681;
+    private static final int REQUEST_CODE_INSTALLER = 0x33;
+
+    private X509Certificate[] mCertificates;
+    private PrivateKey mPrivateKey;
     private WebView mWebView;
+    private AccessibilityManager mAccessibilityManager;
+    private static final int POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE = 101;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        Toast.makeText(this, R.string.vm_creation_message, Toast.LENGTH_SHORT).show();
-        VmLauncherServices.startVmLauncherService(this, this);
+
+        boolean launchInstaller = installIfNecessary();
+        try {
+            // No resize for now.
+            long newSizeInBytes = 0;
+            diskResize(this, newSizeInBytes);
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to resize disk", e);
+            Toast.makeText(this, "Error resizing disk: " + e.getMessage(), Toast.LENGTH_LONG)
+                    .show();
+        }
+
+        checkAndRequestPostNotificationsPermission();
+
+        NotificationManager notificationManager = getSystemService(NotificationManager.class);
+        NotificationChannel notificationChannel =
+                new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_LOW);
+        assert notificationManager != null;
+        notificationManager.createNotificationChannel(notificationChannel);
 
         setContentView(R.layout.activity_headless);
+
+        MaterialToolbar toolbar = (MaterialToolbar) findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
         mWebView = (WebView) findViewById(R.id.webview);
         mWebView.getSettings().setDatabaseEnabled(true);
         mWebView.getSettings().setDomStorageEnabled(true);
         mWebView.getSettings().setJavaScriptEnabled(true);
         mWebView.setWebChromeClient(new WebChromeClient());
+
+        mAccessibilityManager = getSystemService(AccessibilityManager.class);
+        mAccessibilityManager.addTouchExplorationStateChangeListener(this);
+
+        connectToTerminalService();
+        readClientCertificate();
+
+        // if installer is launched, it will be handled in onActivityResult
+        if (!launchInstaller) {
+            startVm();
+        }
+    }
+
+    private URL getTerminalServiceUrl() {
+        boolean needsAccessibility = mAccessibilityManager.isTouchExplorationEnabled();
+        String file = "/";
+        String query = needsAccessibility ? "?screenReaderMode=true" : "";
+
+        try {
+            return new URL("https", VM_ADDR, TTYD_PORT, file + query);
+        } catch (MalformedURLException e) {
+            // this cannot happen
+            return null;
+        }
+    }
+
+    private void readClientCertificate() {
+        // TODO(b/363235314): instead of using the key in asset, it should be generated in runtime
+        // and then provisioned in the vm via virtio-fs
+        try (InputStream keystoreFileStream =
+                getClass().getResourceAsStream("/assets/client.p12")) {
+            KeyStore keyStore = KeyStore.getInstance("PKCS12");
+            String password = "1234";
+            String alias = "1";
+
+            keyStore.load(keystoreFileStream, password != null ? password.toCharArray() : null);
+            Key key = keyStore.getKey(alias, password.toCharArray());
+            if (key instanceof PrivateKey) {
+                mPrivateKey = (PrivateKey) key;
+                Certificate cert = keyStore.getCertificate(alias);
+                mCertificates = new X509Certificate[1];
+                mCertificates[0] = (X509Certificate) cert;
+            }
+        } catch (Exception e) {
+            Log.e(TAG, e.getMessage());
+        }
+    }
+
+    private void connectToTerminalService() {
+        Log.i(TAG, "URL=" + getTerminalServiceUrl().toString());
         mWebView.setWebViewClient(
                 new WebViewClient() {
                     @Override
-                    public boolean shouldOverrideUrlLoading(WebView view, String url) {
-                        view.loadUrl(url);
-                        return true;
+                    public boolean shouldOverrideUrlLoading(
+                            WebView view, WebResourceRequest request) {
+                        return false;
+                    }
+
+                    @Override
+                    public void onReceivedError(
+                            WebView view, WebResourceRequest request, WebResourceError error) {
+                        switch (error.getErrorCode()) {
+                            case WebViewClient.ERROR_CONNECT:
+                            case WebViewClient.ERROR_HOST_LOOKUP:
+                                view.reload();
+                                return;
+                            default:
+                                String url = request.getUrl().toString();
+                                CharSequence msg = error.getDescription();
+                                Log.e(TAG, "Failed to load " + url + ": " + msg);
+                        }
+                    }
+
+                    @Override
+                    public void onPageFinished(WebView view, String url) {
+                        URL loadedUrl = null;
+                        try {
+                            loadedUrl = new URL(url);
+                        } catch (MalformedURLException e) {
+                            // cannot happen.
+                        }
+                        Log.i(TAG, "on page finished. URL=" + loadedUrl);
+                        if (getTerminalServiceUrl().toString().equals(url)) {
+                            android.os.Trace.endAsyncSection("executeTerminal", 0);
+                            view.setVisibility(View.VISIBLE);
+                        }
+                    }
+
+                    @Override
+                    public void onReceivedClientCertRequest(
+                            WebView view, ClientCertRequest request) {
+                        if (mPrivateKey != null && mCertificates != null) {
+                            request.proceed(mPrivateKey, mCertificates);
+                            return;
+                        }
+                        super.onReceivedClientCertRequest(view, request);
+                    }
+
+                    @Override
+                    public void onReceivedSslError(
+                            WebView view, SslErrorHandler handler, SslError error) {
+                        // ttyd uses self-signed certificate
+                        handler.proceed();
                     }
                 });
+        new Thread(
+                        () -> {
+                            waitUntilVmStarts();
+                            runOnUiThread(
+                                    () -> mWebView.loadUrl(getTerminalServiceUrl().toString()));
+                        })
+                .start();
+    }
+
+    private void diskResize(Context context, long sizeInBytes) throws IOException {
+        try {
+            if (sizeInBytes == 0) {
+                return;
+            }
+            File file = getPartitionFile(context, "root_part");
+            String filePath = file.getAbsolutePath();
+            Log.d(TAG, "Disk-resize in progress for partition: " + filePath);
+
+            long currentSize = Os.stat(filePath).st_size;
+            runE2fsck(filePath);
+            if (sizeInBytes > currentSize) {
+                allocateSpace(file, sizeInBytes);
+            }
+
+            resizeFilesystem(filePath, sizeInBytes);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "ErrnoException during disk resize", e);
+            throw new IOException("ErrnoException during disk resize", e);
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to resize disk", e);
+            throw e;
+        }
+    }
+
+    private static File getPartitionFile(Context context, String fileName)
+            throws FileNotFoundException {
+        File file = new File(context.getFilesDir(), fileName);
+        if (!file.exists()) {
+            Log.d(TAG, fileName + " - file not found");
+            throw new FileNotFoundException("File not found: " + fileName);
+        }
+        return file;
+    }
+
+    private static void allocateSpace(File file, long sizeInBytes) throws IOException {
+        try {
+            RandomAccessFile raf = new RandomAccessFile(file, "rw");
+            FileDescriptor fd = raf.getFD();
+            Os.posix_fallocate(fd, 0, sizeInBytes);
+            raf.close();
+            Log.d(TAG, "Allocated space to: " + sizeInBytes + " bytes");
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to allocate space", e);
+            throw new IOException("Failed to allocate space", e);
+        }
+    }
+
+    private static void runE2fsck(String filePath) throws IOException {
+        try {
+            runCommand("/system/bin/e2fsck", "-f", filePath);
+            Log.d(TAG, "e2fsck completed: " + filePath);
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to run e2fsck", e);
+            throw e;
+        }
+    }
+
+    private static void resizeFilesystem(String filePath, long sizeInBytes) throws IOException {
+        long sizeInMB = sizeInBytes / (1024 * 1024);
+        if (sizeInMB == 0) {
+            Log.e(TAG, "Invalid size: " + sizeInBytes + " bytes");
+            throw new IllegalArgumentException("Size cannot be zero MB");
+        }
+        String sizeArg = sizeInMB + "M";
+        try {
+            runCommand("/system/bin/resize2fs", filePath, sizeArg);
+            Log.d(TAG, "resize2fs completed: " + filePath + ", size: " + sizeArg);
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to run resize2fs", e);
+            throw e;
+        }
+    }
+
+    private static void runCommand(String... command) throws IOException {
+        try {
+            Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
+            process.waitFor();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IOException("Command interrupted", e);
+        }
+    }
+
+    private static void waitUntilVmStarts() {
+        InetAddress addr = null;
+        try {
+            addr = InetAddress.getByName(VM_ADDR);
+        } catch (UnknownHostException e) {
+            // this can never happen.
+        }
+        try {
+            while (!addr.isReachable(10000)) {}
+        } catch (IOException e) {
+            // give up on network error
+            throw new RuntimeException(e);
+        }
+        return;
+    }
+
+    private void checkAndRequestPostNotificationsPermission() {
+        if (getApplicationContext().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
+                != PackageManager.PERMISSION_GRANTED) {
+            requestPermissions(
+                    new String[]{Manifest.permission.POST_NOTIFICATIONS},
+                    POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE);
+        }
     }
 
     @Override
     protected void onDestroy() {
+        getSystemService(AccessibilityManager.class).removeTouchExplorationStateChangeListener(this);
         VmLauncherServices.stopVmLauncherService(this);
         super.onDestroy();
     }
 
-    private void gotoURL(String url) {
-        runOnUiThread(() -> mWebView.loadUrl(url));
-    }
-
+    @Override
     public void onVmStart() {
         Log.i(TAG, "onVmStart()");
     }
 
+    @Override
     public void onVmStop() {
         Toast.makeText(this, R.string.vm_stop_message, Toast.LENGTH_SHORT).show();
         Log.i(TAG, "onVmStop()");
         finish();
     }
 
+    @Override
     public void onVmError() {
         Toast.makeText(this, R.string.vm_error_message, Toast.LENGTH_SHORT).show();
         Log.i(TAG, "onVmError()");
         finish();
     }
 
+    @Override
     public void onIpAddrAvailable(String ipAddr) {
-        mVmIpAddr = ipAddr;
-        ((TextView) findViewById(R.id.ip_addr_textview)).setText(mVmIpAddr);
-
-        // TODO(b/359523803): Use AVF API to be notified when shell is ready instead of using dealy
-        new Handler(Looper.getMainLooper())
-                .postDelayed(() -> gotoURL("http://" + mVmIpAddr + ":7681"), 2000);
+        // TODO: remove this
     }
 
     @Override
@@ -101,17 +374,73 @@
     }
 
     @Override
-    public boolean onMenuItemSelected(int featureId, MenuItem item) {
+    public boolean onOptionsItemSelected(MenuItem item) {
         int id = item.getItemId();
-        if (id == R.id.copy_ip_addr) {
-            // TODO(b/340126051): remove this menu item when port forwarding is supported.
-            getSystemService(ClipboardManager.class)
-                    .setPrimaryClip(ClipData.newPlainText("A VM's IP address", mVmIpAddr));
-            return true;
-        } else if (id == R.id.stop_vm) {
-            VmLauncherServices.stopVmLauncherService(this);
+        if (id == R.id.menu_item_settings) {
+            Intent intent = new Intent(this, SettingsActivity.class);
+            this.startActivity(intent);
             return true;
         }
-        return super.onMenuItemSelected(featureId, item);
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    public void onTouchExplorationStateChanged(boolean enabled) {
+        connectToTerminalService();
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+
+        if (requestCode == REQUEST_CODE_INSTALLER) {
+            if (resultCode != RESULT_OK) {
+                Log.e(TAG, "Failed to start VM. Installer returned error.");
+                finish();
+            }
+            startVm();
+        }
+    }
+
+    private boolean installIfNecessary() {
+        // If payload from external storage exists(only for debuggable build) or there is no
+        // installed image, launch installer activity.
+        if ((Build.isDebuggable() && InstallUtils.payloadFromExternalStorageExists())
+                || !InstallUtils.isImageInstalled(this)) {
+            Intent intent = new Intent(this, InstallerActivity.class);
+            startActivityForResult(intent, REQUEST_CODE_INSTALLER);
+            return true;
+        }
+        return false;
+    }
+
+    private void startVm() {
+        if (!InstallUtils.isImageInstalled(this)) {
+            return;
+        }
+        // TODO: implement intent for setting, close and tap to the notification
+        // Currently mock a PendingIntent for notification.
+        Intent intent = new Intent();
+        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent,
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+        Icon icon = Icon.createWithResource(getResources(), R.drawable.ic_launcher_foreground);
+        Notification notification = new Notification.Builder(this, TAG)
+                .setChannelId(TAG)
+                .setSmallIcon(R.drawable.ic_launcher_foreground)
+                .setContentTitle(getResources().getString(R.string.service_notification_title))
+                .setContentText(getResources().getString(R.string.service_notification_content))
+                .setContentIntent(pendingIntent)
+                .setOngoing(true)
+                .addAction(new Notification.Action.Builder(icon,
+                        getResources().getString(R.string.service_notification_settings),
+                        pendingIntent).build())
+                .addAction(new Notification.Action.Builder(icon,
+                        getResources().getString(R.string.service_notification_quit_action),
+                        pendingIntent).build())
+                .build();
+
+        android.os.Trace.beginAsyncSection("executeTerminal", 0);
+        VmLauncherServices.startVmLauncherService(this, this, notification);
     }
 }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsActivity.kt
new file mode 100644
index 0000000..dccfea3
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsActivity.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+package com.android.virtualization.terminal
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
+class SettingsActivity : AppCompatActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.settings_activity)
+
+        val settingsItems = arrayOf(
+            SettingsItem(
+                resources.getString(R.string.settings_disk_resize_title),
+                resources.getString(R.string.settings_disk_resize_sub_title),
+                R.drawable.baseline_storage_24,
+                SettingsItemEnum.DiskResize
+            ),
+            SettingsItem(
+                resources.getString(R.string.settings_port_forwarding_title),
+                resources.getString(R.string.settings_port_forwarding_sub_title),
+                R.drawable.baseline_call_missed_outgoing_24,
+                SettingsItemEnum.PortForwarding
+            ),
+            SettingsItem(
+                resources.getString(R.string.settings_recovery_title),
+                resources.getString(R.string.settings_recovery_sub_title),
+                R.drawable.baseline_settings_backup_restore_24,
+                SettingsItemEnum.Recovery
+            ),
+        )
+        val settingsListItemAdapter = SettingsItemAdapter(settingsItems)
+
+        val recyclerView: RecyclerView = findViewById(R.id.settings_list_recycler_view)
+        recyclerView.layoutManager = LinearLayoutManager(this)
+        recyclerView.adapter = settingsListItemAdapter
+    }
+}
\ No newline at end of file
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
new file mode 100644
index 0000000..1b14ef2
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+package com.android.virtualization.terminal
+
+import android.os.Bundle
+import android.os.FileUtils
+import android.widget.TextView
+import android.widget.Toast
+import android.text.style.RelativeSizeSpan
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.format.Formatter
+import android.text.TextUtils
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.isVisible
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.slider.Slider
+
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+class SettingsDiskResizeActivity : AppCompatActivity() {
+    private val maxDiskSize: Float = 256F
+    private val numberPattern: Pattern = Pattern.compile("[\\d]*[\\Ù«.,]?[\\d]+");
+    private var diskSize: Float = 104F
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.settings_disk_resize)
+        val diskSizeText = findViewById<TextView>(R.id.settings_disk_resize_resize_gb_assigned)
+        val diskMaxSizeText = findViewById<TextView>(R.id.settings_disk_resize_resize_gb_max)
+        diskMaxSizeText.text = getString(R.string.settings_disk_resize_resize_gb_max_format,
+            localizedFileSize(maxDiskSize));
+
+        val diskSizeSlider = findViewById<Slider>(R.id.settings_disk_resize_disk_size_slider)
+        diskSizeSlider.setValueTo(maxDiskSize)
+        val cancelButton = findViewById<MaterialButton>(R.id.settings_disk_resize_cancel_button)
+        val resizeButton = findViewById<MaterialButton>(R.id.settings_disk_resize_resize_button)
+        diskSizeSlider.value = diskSize
+        diskSizeText.text = enlargeFontOfNumber(
+            getString(R.string.settings_disk_resize_resize_gb_assigned_format,
+            localizedFileSize(diskSize)))
+
+        diskSizeSlider.addOnChangeListener { _, value, _ ->
+            diskSizeText.text = enlargeFontOfNumber(
+                getString(R.string.settings_disk_resize_resize_gb_assigned_format,
+                localizedFileSize(value)))
+            cancelButton.isVisible = true
+            resizeButton.isVisible = true
+        }
+        cancelButton.setOnClickListener {
+            diskSizeSlider.value = diskSize
+            cancelButton.isVisible = false
+            resizeButton.isVisible = false
+        }
+
+        resizeButton.setOnClickListener {
+            diskSize = diskSizeSlider.value
+            cancelButton.isVisible = false
+            resizeButton.isVisible = false
+            Toast.makeText(this@SettingsDiskResizeActivity, R.string.settings_disk_resize_resize_message, Toast.LENGTH_SHORT)
+                .show()
+        }
+    }
+
+    fun localizedFileSize(sizeGb: Float): String {
+        // formatShortFileSize() uses SI unit (i.e. kB = 1000 bytes),
+        // so covert sizeGb with "GB" instead of "GIB".
+        val bytes = FileUtils.parseSize(sizeGb.toLong().toString() + "GB")
+        return Formatter.formatShortFileSize(this, bytes)
+    }
+
+    fun enlargeFontOfNumber(summary: CharSequence): CharSequence {
+        if (TextUtils.isEmpty(summary)) {
+            return ""
+        }
+
+        val matcher = numberPattern.matcher(summary);
+        if (matcher.find()) {
+            val spannableSummary = SpannableString(summary)
+            spannableSummary.setSpan(
+                    RelativeSizeSpan(2f),
+                    matcher.start(),
+                    matcher.end(),
+                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+            return spannableSummary
+        }
+        return summary
+    }
+}
\ No newline at end of file
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsItem.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsItem.kt
new file mode 100644
index 0000000..e1723a7
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsItem.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+package com.android.virtualization.terminal
+
+enum class SettingsItemEnum {
+    DiskResize, PortForwarding, Recovery
+}
+
+class SettingsItem(
+    val title: String,
+    val subTitle: String,
+    val icon: Int,
+    val settingsItemEnum: SettingsItemEnum
+) {
+}
\ No newline at end of file
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsItemAdapter.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsItemAdapter.kt
new file mode 100644
index 0000000..86f5c92
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsItemAdapter.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+package com.android.virtualization.terminal
+
+import android.content.Intent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.card.MaterialCardView
+
+class SettingsItemAdapter(private val dataSet: Array<SettingsItem>) :
+    RecyclerView.Adapter<SettingsItemAdapter.ViewHolder>() {
+
+    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+        val card: MaterialCardView = view.findViewById(R.id.settings_list_item_card)
+        val icon: ImageView = view.findViewById(R.id.settings_list_item_icon)
+        val title: TextView = view.findViewById(R.id.settings_list_item_title)
+        val subTitle: TextView = view.findViewById(R.id.settings_list_item_sub_title)
+    }
+
+    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
+        val view = LayoutInflater.from(viewGroup.context)
+            .inflate(R.layout.settings_list_item, viewGroup, false)
+        return ViewHolder(view)
+    }
+
+    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
+        viewHolder.icon.setImageResource(dataSet[position].icon)
+        viewHolder.title.text = dataSet[position].title
+        viewHolder.subTitle.text = dataSet[position].subTitle
+
+        viewHolder.card.setOnClickListener { view ->
+            val intent = Intent(
+                viewHolder.itemView.context,
+                when (dataSet[position].settingsItemEnum) {
+                    SettingsItemEnum.DiskResize -> SettingsDiskResizeActivity::class.java
+                    SettingsItemEnum.PortForwarding -> SettingsPortForwardingActivity::class.java
+                    SettingsItemEnum.Recovery -> SettingsRecoveryActivity::class.java
+                }
+            )
+            view.context.startActivity(intent)
+        }
+    }
+
+    override fun getItemCount() = dataSet.size
+}
\ No newline at end of file
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
new file mode 100644
index 0000000..7119225
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+package com.android.virtualization.terminal
+
+import android.Manifest
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
+class SettingsPortForwardingActivity : AppCompatActivity() {
+    val TAG: String = "VmTerminalApp"
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.settings_port_forwarding)
+
+        val settingsPortForwardingItems = arrayOf(
+            SettingsPortForwardingItem(8080, true),
+            SettingsPortForwardingItem(443, false),
+            SettingsPortForwardingItem(80, false)
+        )
+
+        val settingsPortForwardingAdapter =
+            SettingsPortForwardingAdapter(settingsPortForwardingItems)
+
+        val recyclerView: RecyclerView = findViewById(R.id.settings_port_forwarding_recycler_view)
+        recyclerView.layoutManager = LinearLayoutManager(this)
+        recyclerView.adapter = settingsPortForwardingAdapter
+
+        // TODO: implement intent for accept, deny and tap to the notification
+        // Currently show a mock notification of a port opening
+        val terminalIntent = Intent()
+        val pendingIntent = PendingIntent.getActivity(
+            this, 0, terminalIntent,
+            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+        )
+        val notification =
+            Notification.Builder(this, TAG)
+                .setChannelId(TAG)
+                .setSmallIcon(R.drawable.ic_launcher_foreground)
+                .setContentTitle(resources.getString(R.string.settings_port_forwarding_notification_title))
+                .setContentText(resources.getString(R.string.settings_port_forwarding_notification_content, settingsPortForwardingItems[0].port))
+                .addAction(
+                    Notification.Action.Builder(
+                        Icon.createWithResource(resources, R.drawable.ic_launcher_foreground),
+                        resources.getString(R.string.settings_port_forwarding_notification_accept),
+                        pendingIntent
+                    ).build()
+                )
+                .addAction(
+                    Notification.Action.Builder(
+                        Icon.createWithResource(resources, R.drawable.ic_launcher_foreground),
+                        resources.getString(R.string.settings_port_forwarding_notification_deny),
+                        pendingIntent
+                    ).build()
+                )
+                .build()
+
+        with(NotificationManager.from(this)) {
+            if (ActivityCompat.checkSelfPermission(
+                    this@SettingsPortForwardingActivity, Manifest.permission.POST_NOTIFICATIONS
+                ) == PackageManager.PERMISSION_GRANTED
+            ) {
+                notify(0, notification)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingAdapter.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingAdapter.kt
new file mode 100644
index 0000000..1fa38e3
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingAdapter.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+package com.android.virtualization.terminal
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.materialswitch.MaterialSwitch
+
+class SettingsPortForwardingAdapter(private val dataSet: Array<SettingsPortForwardingItem>) :
+    RecyclerView.Adapter<SettingsPortForwardingAdapter.ViewHolder>() {
+
+    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+        val enabledSwitch: MaterialSwitch =
+            view.findViewById(R.id.settings_port_forwarding_item_enabled_switch)
+        val port: TextView = view.findViewById(R.id.settings_port_forwarding_item_port)
+    }
+
+    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
+        val view = LayoutInflater.from(viewGroup.context)
+            .inflate(R.layout.settings_port_forwarding_item, viewGroup, false)
+        return ViewHolder(view)
+    }
+
+    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
+        viewHolder.port.text = dataSet[position].port.toString()
+        viewHolder.enabledSwitch.isChecked = dataSet[position].enabled
+    }
+
+    override fun getItemCount() = dataSet.size
+}
\ No newline at end of file
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingItem.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingItem.kt
new file mode 100644
index 0000000..599e377
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingItem.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+package com.android.virtualization.terminal
+
+class SettingsPortForwardingItem(val port: Int, val enabled: Boolean) {}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
new file mode 100644
index 0000000..7256015
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+package com.android.virtualization.terminal
+
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import com.google.android.material.card.MaterialCardView
+
+class SettingsRecoveryActivity : AppCompatActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.settings_recovery)
+        val resetCard = findViewById<MaterialCardView>(R.id.settings_recovery_reset_card)
+        resetCard.setOnClickListener {
+            Toast.makeText(this@SettingsRecoveryActivity, R.string.settings_recovery_reset_message, Toast.LENGTH_SHORT).show()
+        }
+    }
+}
\ No newline at end of file
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SplitInitializer.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SplitInitializer.kt
new file mode 100644
index 0000000..cb917bd
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SplitInitializer.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+package com.android.virtualization.terminal
+
+import android.content.Context
+import androidx.startup.Initializer
+import androidx.window.embedding.RuleController
+
+class SplitInitializer : Initializer<RuleController> {
+
+    override fun create(context: Context): RuleController {
+        return RuleController.getInstance(context).apply {
+            setRules(RuleController.parseRules(context, R.xml.main_split_config))
+        }
+    }
+
+    override fun dependencies(): List<Class<out Initializer<*>>> {
+        return emptyList()
+    }
+}
\ No newline at end of file
diff --git a/android/TerminalApp/res/drawable/baseline_call_missed_outgoing_24.xml b/android/TerminalApp/res/drawable/baseline_call_missed_outgoing_24.xml
new file mode 100644
index 0000000..597c317
--- /dev/null
+++ b/android/TerminalApp/res/drawable/baseline_call_missed_outgoing_24.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+    <path android:fillColor="@android:color/white" android:pathData="M3,8.41l9,9l7,-7V15h2V7h-8v2h4.59L12,14.59L4.41,7L3,8.41z"/>
+
+</vector>
diff --git a/android/TerminalApp/res/drawable/baseline_settings_backup_restore_24.xml b/android/TerminalApp/res/drawable/baseline_settings_backup_restore_24.xml
new file mode 100644
index 0000000..22b23ba
--- /dev/null
+++ b/android/TerminalApp/res/drawable/baseline_settings_backup_restore_24.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+    <path android:fillColor="@android:color/white" android:pathData="M14,12c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2 0.9,2 2,2 2,-0.9 2,-2zM12,3c-4.97,0 -9,4.03 -9,9L0,12l4,4 4,-4L5,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.51,0 -2.91,-0.49 -4.06,-1.3l-1.42,1.44C8.04,20.3 9.94,21 12,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9z"/>
+
+</vector>
diff --git a/android/TerminalApp/res/drawable/baseline_storage_24.xml b/android/TerminalApp/res/drawable/baseline_storage_24.xml
new file mode 100644
index 0000000..6e52e3f
--- /dev/null
+++ b/android/TerminalApp/res/drawable/baseline_storage_24.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+    <path android:fillColor="@android:color/white" android:pathData="M2,20h20v-4L2,16v4zM4,17h2v2L4,19v-2zM2,4v4h20L22,4L2,4zM6,7L4,7L4,5h2v2zM2,14h20v-4L2,10v4zM4,11h2v2L4,13v-2z"/>
+
+</vector>
diff --git a/android/TerminalApp/res/drawable/ic_settings.xml b/android/TerminalApp/res/drawable/ic_settings.xml
new file mode 100644
index 0000000..4bcd4aa
--- /dev/null
+++ b/android/TerminalApp/res/drawable/ic_settings.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM440,800L519,800L533,694Q564,686 590.5,670.5Q617,655 639,633L738,674L777,606L691,541Q696,527 698,511.5Q700,496 700,480Q700,464 698,448.5Q696,433 691,419L777,354L738,286L639,328Q617,305 590.5,289.5Q564,274 533,266L520,160L441,160L427,266Q396,274 369.5,289.5Q343,305 321,327L222,286L183,354L269,418Q264,433 262,448Q260,463 260,480Q260,496 262,511Q264,526 269,541L183,606L222,674L321,632Q343,655 369.5,670.5Q396,686 427,694L440,800ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620ZM480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Z"/>
+</vector>
diff --git a/android/TerminalApp/res/layout/activity_headless.xml b/android/TerminalApp/res/layout/activity_headless.xml
index 3fe5271..7baaf5c 100644
--- a/android/TerminalApp/res/layout/activity_headless.xml
+++ b/android/TerminalApp/res/layout/activity_headless.xml
@@ -7,14 +7,37 @@
     android:orientation="vertical"
     android:fitsSystemWindows="true"
     tools:context=".MainActivity">
-    <TextView
-        android:id="@+id/ip_addr_textview"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content" />
-  <WebView
-      android:id="@+id/webview"
-      android:layout_width="match_parent"
-      android:layout_height="match_parent"
-      android:layout_marginBottom="5dp" />
+    <com.google.android.material.appbar.MaterialToolbar
+        android:id="@+id/toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="?attr/actionBarSize"
+        app:layout_constraintTop_toTopOf="parent"/>
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+        <LinearLayout
+            android:orientation="vertical"
+            android:gravity="center"
+            android:layout_gravity="center"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content">
+            <com.google.android.material.textview.MaterialTextView
+                android:text="@string/vm_creation_message"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="5dp"/>
+            <com.google.android.material.progressindicator.CircularProgressIndicator
+                android:indeterminate="true"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"/>
+        </LinearLayout>
+        <WebView
+            android:id="@+id/webview"
+            android:layout_marginBottom="5dp"
+            android:layout_gravity="fill"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="invisible"/>
+    </FrameLayout>
 
 </LinearLayout>
diff --git a/android/FerrochromeApp/res/layout/activity_ferrochrome.xml b/android/TerminalApp/res/layout/activity_installer.xml
similarity index 100%
rename from android/FerrochromeApp/res/layout/activity_ferrochrome.xml
rename to android/TerminalApp/res/layout/activity_installer.xml
diff --git a/android/TerminalApp/res/layout/settings_activity.xml b/android/TerminalApp/res/layout/settings_activity.xml
new file mode 100644
index 0000000..4fc6229
--- /dev/null
+++ b/android/TerminalApp/res/layout/settings_activity.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:fitsSystemWindows="true">
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/settings_list_recycler_view"
+        android:layout_marginHorizontal="16dp"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+</LinearLayout>
\ No newline at end of file
diff --git a/android/TerminalApp/res/layout/settings_disk_resize.xml b/android/TerminalApp/res/layout/settings_disk_resize.xml
new file mode 100644
index 0000000..f868b28
--- /dev/null
+++ b/android/TerminalApp/res/layout/settings_disk_resize.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:orientation="vertical"
+    android:layout_marginStart="24dp"
+    android:layout_marginEnd="24dp"
+    android:layout_marginTop="24dp"
+    android:fitsSystemWindows="true">
+
+    <TextView
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:text="@string/settings_disk_resize_title"
+        android:textSize="48sp"
+        android:layout_marginBottom="24dp"/>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <TextView
+            android:id="@+id/settings_disk_resize_resize_gb_assigned"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:textSize="14sp"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintBottom_toTopOf="@+id/settings_disk_resize_disk_size_slider"/>
+
+        <TextView
+            android:id="@+id/settings_disk_resize_resize_gb_max"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:textSize="14sp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintBottom_toTopOf="@+id/settings_disk_resize_disk_size_slider"/>
+
+        <com.google.android.material.slider.Slider
+            android:id="@+id/settings_disk_resize_disk_size_slider"
+            android:layout_height="wrap_content"
+            android:layout_width="match_parent"
+            android:layout_marginBottom="36dp"
+            app:tickVisible="false"
+            android:valueFrom="0"
+            android:stepSize="4"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/settings_disk_resize_cancel_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/settings_disk_resize_resize_cancel"
+            android:visibility="invisible"
+            android:layout_marginHorizontal="8dp"
+            app:layout_constraintTop_toTopOf="@+id/settings_disk_resize_disk_size_slider"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@+id/settings_disk_resize_resize_button" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/settings_disk_resize_resize_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/settings_disk_resize_resize_restart_vm_to_apply"
+            android:visibility="invisible"
+            android:layout_marginHorizontal="8dp"
+            app:layout_constraintTop_toTopOf="@+id/settings_disk_resize_disk_size_slider"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/android/TerminalApp/res/layout/settings_list_item.xml b/android/TerminalApp/res/layout/settings_list_item.xml
new file mode 100644
index 0000000..89f2d82
--- /dev/null
+++ b/android/TerminalApp/res/layout/settings_list_item.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:gravity="center_vertical"
+    android:layout_height="wrap_content">
+
+    <com.google.android.material.card.MaterialCardView
+        android:id="@+id/settings_list_item_card"
+        app:strokeWidth="0dp"
+        app:cardCornerRadius="28dp"
+        app:checkedIcon="@null"
+        android:focusable="true"
+        android:checkable="true"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="88dp"
+            android:layout_marginStart="24dp"
+            android:layout_marginEnd="16dp">
+
+            <com.google.android.material.imageview.ShapeableImageView
+                android:id="@+id/settings_list_item_icon"
+                android:layout_width="24dp"
+                android:layout_height="24dp"
+                android:layout_marginEnd="24dp"
+                android:scaleType="centerCrop"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintLeft_toLeftOf="parent" />
+
+            <TextView
+                android:id="@+id/settings_list_item_title"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="20dp"
+                android:layout_marginStart="24dp"
+                android:textSize="20sp"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toTopOf="@+id/settings_list_item_sub_title"
+                app:layout_constraintStart_toEndOf="@id/settings_list_item_icon"
+                app:layout_constraintEnd_toEndOf="parent" />
+
+            <TextView
+                android:id="@+id/settings_list_item_sub_title"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:textSize="14sp"
+                android:layout_marginBottom="20dp"
+                android:layout_marginStart="24dp"
+                app:layout_constraintTop_toBottomOf="@+id/settings_list_item_title"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toEndOf="@id/settings_list_item_icon"
+                app:layout_constraintEnd_toEndOf="parent" />
+        </androidx.constraintlayout.widget.ConstraintLayout>
+    </com.google.android.material.card.MaterialCardView>
+</FrameLayout>
\ No newline at end of file
diff --git a/android/TerminalApp/res/layout/settings_port_forwarding.xml b/android/TerminalApp/res/layout/settings_port_forwarding.xml
new file mode 100644
index 0000000..1d68907
--- /dev/null
+++ b/android/TerminalApp/res/layout/settings_port_forwarding.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:layout_marginStart="24dp"
+    android:layout_marginEnd="24dp"
+    android:layout_marginTop="24dp"
+    android:fitsSystemWindows="true">
+
+    <TextView
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:text="@string/settings_port_forwarding_title"
+        android:textSize="48sp"
+        android:layout_marginBottom="24dp"/>
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/settings_port_forwarding_recycler_view"
+        android:layout_marginHorizontal="16dp"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+</LinearLayout>
\ No newline at end of file
diff --git a/android/TerminalApp/res/layout/settings_port_forwarding_item.xml b/android/TerminalApp/res/layout/settings_port_forwarding_item.xml
new file mode 100644
index 0000000..9e5981e
--- /dev/null
+++ b/android/TerminalApp/res/layout/settings_port_forwarding_item.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    app:layout_constraintCircleRadius="@dimen/material_emphasis_medium"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <TextView
+        android:id="@+id/settings_port_forwarding_item_port"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"/>
+
+    <com.google.android.material.materialswitch.MaterialSwitch
+        android:id="@+id/settings_port_forwarding_item_enabled_switch"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintRight_toRightOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/android/TerminalApp/res/layout/settings_recovery.xml b/android/TerminalApp/res/layout/settings_recovery.xml
new file mode 100644
index 0000000..e18c8a6
--- /dev/null
+++ b/android/TerminalApp/res/layout/settings_recovery.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:orientation="vertical"
+    android:layout_marginEnd="24dp"
+    android:layout_marginTop="24dp"
+    android:fitsSystemWindows="true">
+
+    <TextView
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:text="@string/settings_recovery_title"
+        android:textSize="48sp"
+        android:layout_marginStart="24dp"
+        android:layout_marginBottom="24dp"/>
+
+    <com.google.android.material.card.MaterialCardView
+        android:id="@+id/settings_recovery_reset_card"
+        app:strokeWidth="0dp"
+        app:cardCornerRadius="0dp"
+        app:checkedIcon="@null"
+        android:focusable="true"
+        android:checkable="true"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="88dp"
+            android:layout_marginEnd="16dp"
+            android:layout_marginStart="24dp">
+
+            <TextView
+                android:id="@+id/settings_recovery_reset_title"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="20dp"
+                android:layout_marginStart="24dp"
+                android:textSize="20sp"
+                android:text="@string/settings_recovery_reset_title"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toTopOf="@+id/settings_recovery_reset_sub_title"
+                app:layout_constraintLeft_toLeftOf="parent" />
+
+            <TextView
+                android:id="@+id/settings_recovery_reset_sub_title"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:textSize="14sp"
+                android:layout_marginBottom="20dp"
+                android:layout_marginStart="24dp"
+                android:text="@string/settings_recovery_reset_sub_title"
+                app:layout_constraintTop_toBottomOf="@+id/settings_recovery_reset_title"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintLeft_toLeftOf="parent" />
+        </androidx.constraintlayout.widget.ConstraintLayout>
+    </com.google.android.material.card.MaterialCardView>
+</LinearLayout>
\ No newline at end of file
diff --git a/android/TerminalApp/res/menu/main_menu.xml b/android/TerminalApp/res/menu/main_menu.xml
index cc34cda..0fee90e 100644
--- a/android/TerminalApp/res/menu/main_menu.xml
+++ b/android/TerminalApp/res/menu/main_menu.xml
@@ -1,7 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:id="@+id/copy_ip_addr"
-        android:title="Copy the IP address"/>
-    <item android:id="@+id/stop_vm"
-        android:title="Stop the existing VM instance"/>
-</menu>
\ No newline at end of file
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <item android:id="@+id/menu_item_settings"
+        android:icon="@drawable/ic_settings"
+        android:title="@string/action_settings"
+        app:showAsAction="always"/>
+</menu>
diff --git a/android/TerminalApp/res/values/dimens.xml b/android/TerminalApp/res/values/dimens.xml
new file mode 100644
index 0000000..e6ed461
--- /dev/null
+++ b/android/TerminalApp/res/values/dimens.xml
@@ -0,0 +1,3 @@
+<resources>
+    <dimen name="activity_split_ratio">0.3</dimen>
+</resources>
\ No newline at end of file
diff --git a/android/TerminalApp/res/values/integers.xml b/android/TerminalApp/res/values/integers.xml
new file mode 100644
index 0000000..0c7d2b9
--- /dev/null
+++ b/android/TerminalApp/res/values/integers.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <integer name="split_min_width">720</integer>
+</resources>
\ No newline at end of file
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
index 79da7cd..0cdb939 100644
--- a/android/TerminalApp/res/values/strings.xml
+++ b/android/TerminalApp/res/values/strings.xml
@@ -16,8 +16,65 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <!-- Application name of this terminal app shown in the launcher. This app provides computer terminal to connect to virtual machine. [CHAR LIMIT=16] -->
     <string name="app_name">Terminal</string>
-    <string name="vm_creation_message">Virtual machine is booting. Please wait.</string>
-    <string name="vm_stop_message">Virtual machine is stopped. Exiting.</string>
-    <string name="vm_error_message">Virtual machine crashed. Exiting.</string>
+
+    <!-- Action bar icon name for the settings view CHAR LIMIT=none] -->
+    <string name="action_settings">Settings</string>
+
+    <!-- Toast message to notify that preparing terminal to start [CHAR LIMIT=none] -->
+    <string name="vm_creation_message">Preparing terminal</string>
+    <!-- Toast message to notify that terminal is stopping [CHAR LIMIT=none] -->
+    <string name="vm_stop_message">Stopping terminal</string>
+    <!-- Toast message to notify that terminal is crashed [CHAR LIMIT=none] -->
+    <string name="vm_error_message">Terminal crashed</string>
+
+    <!-- Settings memu title for resizing disk of the virtual machine. [CHAR LIMIT=none] -->
+    <string name="settings_disk_resize_title">Disk Resize</string>
+    <!-- Settings memu subtitle for resizing disk of the virtual machine. [CHAR LIMIT=none] -->
+    <string name="settings_disk_resize_sub_title">Resize / Rootfs</string>
+    <!-- Toast message after new disk size is set. [CHAR LIMIT=none] -->
+    <string name="settings_disk_resize_resize_message">Disk size set</string>
+    <!-- Settings menu option description format of the current disk size. [CHAR LIMIT=none] -->
+    <string name="settings_disk_resize_resize_gb_assigned_format"><xliff:g id="assigned_size" example="10GB">%1$s</xliff:g> assigned</string>
+    <!-- Settings menu option description format of the maximum resizable disk size. [CHAR LIMIT=none] -->
+    <string name="settings_disk_resize_resize_gb_max_format"><xliff:g id="max_size" example="256GB">%1$s</xliff:g> max</string>
+    <!-- Settings menu button to cancel disk resize. [CHAR LIMIT=32] -->
+    <string name="settings_disk_resize_resize_cancel">Cancel</string>
+    <!-- Settings menu button to apply change that requires to restart VM (abbrev of virtual machine). [CHAR LIMIT=64] -->
+    <string name="settings_disk_resize_resize_restart_vm_to_apply">Restart VM to apply</string>
+
+    <!-- Settings menu title for 'port forwarding' [CHAR LIMIT=none] -->
+    <string name="settings_port_forwarding_title">Port Forwarding</string>
+    <!-- Settings menu subtitle for 'port forwarding' [CHAR LIMIT=none] -->
+    <string name="settings_port_forwarding_sub_title">Configure port forwarding</string>
+    <!-- Notification title for new port forwarding [CHAR LIMIT=none] -->
+    <string name="settings_port_forwarding_notification_title">Terminal is trying to open a new port</string>
+    <!-- Notification content for new port forwarding [CHAR LIMIT=none] -->
+    <string name="settings_port_forwarding_notification_content">Port requested to be open: <xliff:g id="port_number" example="8080">%d</xliff:g></string>
+    <!-- Notification action accept [CHAR LIMIT=none] -->
+    <string name="settings_port_forwarding_notification_accept">Accept</string>
+    <!-- Notification action deny [CHAR LIMIT=none] -->
+    <string name="settings_port_forwarding_notification_deny">Deny</string>
+
+    <!-- Settings menu title for recoverying image [CHAR LIMIT=none] -->
+    <string name="settings_recovery_title">Recovery</string>
+    <!-- Settings menu subtitle for recoverying image [CHAR LIMIT=none] -->
+    <string name="settings_recovery_sub_title">Partition Recovery options</string>
+    <!-- Settings menu title for resetting the virtual machine image [CHAR LIMIT=none] -->
+    <string name="settings_recovery_reset_title">Change to Initial version</string>
+    <!-- Settings menu subtitle for resetting the virtual machine image [CHAR LIMIT=none] -->
+    <string name="settings_recovery_reset_sub_title">Remove all</string>
+    <!-- Toast message for reset is completed [CHAR LIMIT=none] -->
+    <string name="settings_recovery_reset_message">VM reset</string>
+
+    <!-- Notification action button for settings [CHAR LIMIT=none] -->
+    <string name="service_notification_settings">Settings</string>
+    <!-- Notification title for foreground service notification [CHAR LIMIT=none] -->
+    <string name="service_notification_title">Terminal is running</string>
+    <!-- Notification content for foreground service notification [CHAR LIMIT=none] -->
+    <string name="service_notification_content">Click to open the terminal.</string>
+    <!-- Notification action button for closing the virtual machine [CHAR LIMIT=none] -->
+    <string name="service_notification_quit_action">Close</string>
 </resources>
diff --git a/android/TerminalApp/res/xml/main_split_config.xml b/android/TerminalApp/res/xml/main_split_config.xml
new file mode 100644
index 0000000..f51e7ea
--- /dev/null
+++ b/android/TerminalApp/res/xml/main_split_config.xml
@@ -0,0 +1,37 @@
+<resources xmlns:window="http://schemas.android.com/apk/res-auto">
+
+    <!-- Define a split for the named activities. -->
+    <ActivityRule window:alwaysExpand="true">
+        <ActivityFilter window:activityName=".MainActivity" />
+    </ActivityRule>
+
+    <SplitPairRule
+        window:clearTop="true"
+        window:finishPrimaryWithSecondary="adjacent"
+        window:finishSecondaryWithPrimary="always"
+        window:splitLayoutDirection="locale"
+        window:splitMaxAspectRatioInPortrait="alwaysAllow"
+        window:splitMinWidthDp="@integer/split_min_width"
+        window:splitRatio="@dimen/activity_split_ratio">
+        <SplitPairFilter
+            window:primaryActivityName="com.android.virtualization.terminal.SettingsActivity"
+            window:secondaryActivityName="com.android.virtualization.terminal.SettingsDiskResizeActivity" />
+        <SplitPairFilter
+            window:primaryActivityName="com.android.virtualization.terminal.SettingsActivity"
+            window:secondaryActivityName="com.android.virtualization.terminal.SettingsPortForwardingActivity" />
+        <SplitPairFilter
+            window:primaryActivityName="com.android.virtualization.terminal.SettingsActivity"
+            window:secondaryActivityName="com.android.virtualization.terminal.SettingsRecoveryActivity" />
+    </SplitPairRule>
+
+    <SplitPlaceholderRule
+        window:placeholderActivityName="com.android.virtualization.terminal.SettingsDiskResizeActivity"
+        window:splitLayoutDirection="locale"
+        window:splitMaxAspectRatioInPortrait="alwaysAllow"
+        window:splitMinWidthDp="@integer/split_min_width"
+        window:splitRatio="@dimen/activity_split_ratio">
+        window:stickyPlaceholder="false">
+        <ActivityFilter
+            window:activityName="com.android.virtualization.terminal.SettingsActivity"/>
+    </SplitPlaceholderRule>
+</resources>
\ No newline at end of file
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index 87fb611..52bfd87 100644
--- a/android/virtmgr/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -33,7 +33,7 @@
     CpuTopology::CpuTopology,
     DiskImage::DiskImage,
     InputDevice::InputDevice,
-    IVirtualMachine::{BnVirtualMachine, IVirtualMachine},
+    IVirtualMachine::{self, BnVirtualMachine},
     IVirtualMachineCallback::IVirtualMachineCallback,
     IVirtualizationService::IVirtualizationService,
     Partition::Partition,
@@ -62,12 +62,12 @@
 use apkverify::{HashAlgorithm, V4Signature};
 use avflog::LogResult;
 use binder::{
-    self, wait_for_interface, BinderFeatures, ExceptionCode, Interface, ParcelFileDescriptor,
-    Status, StatusCode, Strong,
-    IntoBinderResult,
+    self, wait_for_interface, Accessor, BinderFeatures, ConnectionInfo, ExceptionCode, Interface, ParcelFileDescriptor,
+    SpIBinder, Status, StatusCode, Strong, IntoBinderResult,
 };
 use cstr::cstr;
 use glob::glob;
+use libc::{AF_VSOCK, sa_family_t, sockaddr_vm};
 use log::{debug, error, info, warn};
 use microdroid_payload_config::{ApkConfig, Task, TaskType, VmPayloadConfig};
 use nix::unistd::pipe;
@@ -98,6 +98,9 @@
 
 pub const BINDER_SERVICE_IDENTIFIER: &str = "android.system.virtualizationservice";
 
+/// Vsock privileged ports are below this number.
+const VSOCK_PRIV_PORT_MAX: u32 = 1024;
+
 /// The size of zero.img.
 /// Gaps in composite disk images are filled with a shared zero.img.
 const ZERO_FILLER_SIZE: u64 = 4096;
@@ -221,7 +224,8 @@
         console_out_fd: Option<&ParcelFileDescriptor>,
         console_in_fd: Option<&ParcelFileDescriptor>,
         log_fd: Option<&ParcelFileDescriptor>,
-    ) -> binder::Result<Strong<dyn IVirtualMachine>> {
+        dump_dt_fd: Option<&ParcelFileDescriptor>,
+    ) -> binder::Result<Strong<dyn IVirtualMachine::IVirtualMachine>> {
         let mut is_protected = false;
         let ret = self.create_vm_internal(
             config,
@@ -229,6 +233,7 @@
             console_in_fd,
             log_fd,
             &mut is_protected,
+            dump_dt_fd,
         );
         write_vm_creation_stats(config, is_protected, &ret);
         ret
@@ -485,7 +490,8 @@
         console_in_fd: Option<&ParcelFileDescriptor>,
         log_fd: Option<&ParcelFileDescriptor>,
         is_protected: &mut bool,
-    ) -> binder::Result<Strong<dyn IVirtualMachine>> {
+        dump_dt_fd: Option<&ParcelFileDescriptor>,
+    ) -> binder::Result<Strong<dyn IVirtualMachine::IVirtualMachine>> {
         let requester_uid = get_calling_uid();
         let requester_debug_pid = get_calling_pid();
 
@@ -527,6 +533,7 @@
             clone_or_prepare_logger_fd(console_out_fd, format!("Console({})", cid))?;
         let console_in_fd = console_in_fd.map(clone_file).transpose()?;
         let log_fd = clone_or_prepare_logger_fd(log_fd, format!("Log({})", cid))?;
+        let dump_dt_fd = dump_dt_fd.map(clone_file).transpose()?;
 
         // Counter to generate unique IDs for temporary image files.
         let mut next_temporary_image_id = 0;
@@ -744,6 +751,7 @@
             audio_config,
             no_balloon: config.noBalloon,
             usb_config,
+            dump_dt_fd,
         };
         let instance = Arc::new(
             VmInstance::new(
@@ -1326,14 +1334,14 @@
 }
 
 impl VirtualMachine {
-    fn create(instance: Arc<VmInstance>) -> Strong<dyn IVirtualMachine> {
+    fn create(instance: Arc<VmInstance>) -> Strong<dyn IVirtualMachine::IVirtualMachine> {
         BnVirtualMachine::new_binder(VirtualMachine { instance }, BinderFeatures::default())
     }
 }
 
 impl Interface for VirtualMachine {}
 
-impl IVirtualMachine for VirtualMachine {
+impl IVirtualMachine::IVirtualMachine for VirtualMachine {
     fn getCid(&self) -> binder::Result<i32> {
         // Don't check permission. The owner of the VM might have passed this binder object to
         // others.
@@ -1394,19 +1402,54 @@
 
     fn connectVsock(&self, port: i32) -> binder::Result<ParcelFileDescriptor> {
         if !matches!(&*self.instance.vm_state.lock().unwrap(), VmState::Running { .. }) {
-            return Err(anyhow!("VM is not running")).or_service_specific_exception(-1);
+            return Err(Status::new_service_specific_error_str(
+                IVirtualMachine::ERROR_UNEXPECTED,
+                Some("Virtual Machine is not running"),
+            ));
         }
         let port = port as u32;
-        if port < 1024 {
-            return Err(anyhow!("Can't connect to privileged port {port}"))
-                .or_service_specific_exception(-1);
+        if port < VSOCK_PRIV_PORT_MAX {
+            return Err(Status::new_service_specific_error_str(
+                IVirtualMachine::ERROR_UNEXPECTED,
+                Some("Can't connect to privileged port {port}"),
+            ));
         }
         let stream = VsockStream::connect_with_cid_port(self.instance.cid, port)
             .context("Failed to connect")
-            .or_service_specific_exception(-1)?;
+            .or_service_specific_exception(IVirtualMachine::ERROR_UNEXPECTED)?;
         Ok(vsock_stream_to_pfd(stream))
     }
 
+    fn createAccessorBinder(&self, name: &str, port: i32) -> binder::Result<SpIBinder> {
+        if !matches!(&*self.instance.vm_state.lock().unwrap(), VmState::Running { .. }) {
+            return Err(Status::new_service_specific_error_str(
+                IVirtualMachine::ERROR_UNEXPECTED,
+                Some("Virtual Machine is not running"),
+            ));
+        }
+        let port = port as u32;
+        if port < VSOCK_PRIV_PORT_MAX {
+            return Err(Status::new_service_specific_error_str(
+                IVirtualMachine::ERROR_UNEXPECTED,
+                Some("Can't connect to privileged port {port}"),
+            ));
+        }
+        let cid = self.instance.cid;
+        let addr = sockaddr_vm {
+            svm_family: AF_VSOCK as sa_family_t,
+            svm_reserved1: 0,
+            svm_port: port,
+            svm_cid: cid,
+            svm_zero: [0u8; 4],
+        };
+        let get_connection_info = move |_instance: &str| Some(ConnectionInfo::Vsock(addr));
+        let accessor = Accessor::new(name, get_connection_info);
+        accessor
+            .as_binder()
+            .context("The newly created Accessor should always have a binder")
+            .or_service_specific_exception(IVirtualMachine::ERROR_UNEXPECTED)
+    }
+
     fn setHostConsoleName(&self, ptsname: &str) -> binder::Result<()> {
         self.instance.vm_context.global_context.setHostConsoleName(ptsname)
     }
diff --git a/android/virtmgr/src/crosvm.rs b/android/virtmgr/src/crosvm.rs
index b2be736..2fad4b9 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -135,6 +135,7 @@
     pub audio_config: Option<AudioConfig>,
     pub no_balloon: bool,
     pub usb_config: UsbConfig,
+    pub dump_dt_fd: Option<File>,
 }
 
 #[derive(Debug)]
@@ -985,6 +986,11 @@
     // Keep track of what file descriptors should be mapped to the crosvm process.
     let mut preserved_fds = config.indirect_files.into_iter().map(|f| f.into()).collect();
 
+    if let Some(dump_dt_fd) = config.dump_dt_fd {
+        let dump_dt_fd = add_preserved_fd(&mut preserved_fds, dump_dt_fd);
+        command.arg("--dump-device-tree-blob").arg(dump_dt_fd);
+    }
+
     // Setup the serial devices.
     // 1. uart device: used as the output device by bootloaders and as early console by linux
     // 2. uart device: used to report the reason for the VM failing.
@@ -1056,6 +1062,8 @@
         command.arg(add_preserved_fd(&mut preserved_fds, kernel));
     }
 
+    command.arg("--no-pmu");
+
     let control_sock = create_crosvm_control_listener(crosvm_control_socket_path)
         .context("failed to create control listener")?;
     command.arg("--socket").arg(add_preserved_fd(&mut preserved_fds, control_sock));
diff --git a/android/virtmgr/src/payload.rs b/android/virtmgr/src/payload.rs
index 81e02b7..5811314 100644
--- a/android/virtmgr/src/payload.rs
+++ b/android/virtmgr/src/payload.rs
@@ -178,14 +178,9 @@
             let pm =
                 wait_for_interface::<dyn IPackageManagerNative>(PACKAGE_MANAGER_NATIVE_SERVICE)
                     .context("Failed to get service when prefer_staged is set.")?;
-            let staged =
-                pm.getStagedApexModuleNames().context("getStagedApexModuleNames failed")?;
-            for name in staged {
-                if let Some(staged_apex_info) =
-                    pm.getStagedApexInfo(&name).context("getStagedApexInfo failed")?
-                {
-                    list.override_staged_apex(&staged_apex_info)?;
-                }
+            let staged = pm.getStagedApexInfos().context("getStagedApexInfos failed")?;
+            for apex in staged {
+                list.override_staged_apex(&apex)?;
             }
         }
         Ok(list)
diff --git a/android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
index afa25e2..e52222a 100644
--- a/android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
+++ b/android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
@@ -19,6 +19,13 @@
 import android.system.virtualizationservice.VirtualMachineState;
 
 interface IVirtualMachine {
+    /**
+     * Encountered an unexpected error. This is an implementation detail and the client
+     * can do nothing about it.
+     * This is used as a Service Specific Exception.
+     */
+    const int ERROR_UNEXPECTED = -1;
+
     /** Get the CID allocated to the VM. */
     int getCid();
 
@@ -48,6 +55,19 @@
     /** Open a vsock connection to the CID of the VM on the given port. */
     ParcelFileDescriptor connectVsock(int port);
 
+    /**
+     * Create an Accessor in libbinder that will open a vsock connection
+     * to the CID of the VM on the given port.
+     *
+     * \param instance name of the service that the accessor is responsible for.
+     *        This is the same instance that we expect clients to use when trying
+     *        to get the service with the ServiceManager APIs.
+     *
+     * \return IBinder of the IAccessor on success, or throws a service specific exception
+     *         on error. See the ERROR_* values above.
+     */
+    IBinder createAccessorBinder(String instance, int port);
+
     /** Set the name of the peer end (ptsname) of the host console. */
     void setHostConsoleName(in @utf8InCpp String pathname);
 
diff --git a/android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
index 234d8d0..0c3f6b7 100644
--- a/android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
+++ b/android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
@@ -35,11 +35,14 @@
      * `consoleInFd` is provided then console input to the VM will be read from it. If `osLogFd` is
      * provided then the OS-level logs will be sent to it. `osLogFd` is supported only when the OS
      * running in the VM has the logging system. In case of Microdroid, the logging system is logd.
+     * `dumpDtFd` is the file where to dump the VM's device tree. It is only used in
+     * debugging/testing.
      */
     IVirtualMachine createVm(in VirtualMachineConfig config,
             in @nullable ParcelFileDescriptor consoleOutFd,
             in @nullable ParcelFileDescriptor consoleInFd,
-            in @nullable ParcelFileDescriptor osLogFd);
+            in @nullable ParcelFileDescriptor osLogFd,
+            in @nullable ParcelFileDescriptor dumpDtFd);
 
     /**
      * Allocate an instance_id to the (newly created) VM.
diff --git a/android/vm/src/main.rs b/android/vm/src/main.rs
index 609bbdf..81ca8fa 100644
--- a/android/vm/src/main.rs
+++ b/android/vm/src/main.rs
@@ -114,6 +114,10 @@
     #[cfg(debuggable_vms_improvements)]
     #[arg(long)]
     enable_earlycon: bool,
+
+    /// Path to file to dump VM device tree.
+    #[arg(long)]
+    dump_device_tree: Option<PathBuf>,
 }
 
 impl DebugConfig {
diff --git a/android/vm/src/run.rs b/android/vm/src/run.rs
index 823546f..0e1f4cc 100644
--- a/android/vm/src/run.rs
+++ b/android/vm/src/run.rs
@@ -203,6 +203,7 @@
         config.debug.console.as_ref().map(|p| p.as_ref()),
         config.debug.console_in.as_ref().map(|p| p.as_ref()),
         config.debug.log.as_ref().map(|p| p.as_ref()),
+        config.debug.dump_device_tree.as_ref().map(|p| p.as_ref()),
     )
 }
 
@@ -284,6 +285,7 @@
         config.debug.console.as_ref().map(|p| p.as_ref()),
         config.debug.console_in.as_ref().map(|p| p.as_ref()),
         config.debug.log.as_ref().map(|p| p.as_ref()),
+        config.debug.dump_device_tree.as_ref().map(|p| p.as_ref()),
     )
 }
 
@@ -306,6 +308,7 @@
     console_out_path: Option<&Path>,
     console_in_path: Option<&Path>,
     log_path: Option<&Path>,
+    dump_device_tree: Option<&Path>,
 ) -> Result<(), Error> {
     let console_out = if let Some(console_out_path) = console_out_path {
         Some(File::create(console_out_path).with_context(|| {
@@ -330,9 +333,17 @@
     } else {
         Some(duplicate_fd(io::stdout())?)
     };
+    let dump_dt = if let Some(dump_device_tree) = dump_device_tree {
+        Some(File::create(dump_device_tree).with_context(|| {
+            format!("Failed to open file to dump device tree: {:?}", dump_device_tree)
+        })?)
+    } else {
+        None
+    };
     let callback = Box::new(Callback {});
-    let vm = VmInstance::create(service, config, console_out, console_in, log, Some(callback))
-        .context("Failed to create VM")?;
+    let vm =
+        VmInstance::create(service, config, console_out, console_in, log, dump_dt, Some(callback))
+            .context("Failed to create VM")?;
     vm.start().context("Failed to start VM")?;
 
     let debug_level = get_debug_level(config).unwrap_or(DebugLevel::NONE);
diff --git a/android/vm_demo_native/main.cpp b/android/vm_demo_native/main.cpp
index bc42036..d7ff02e 100644
--- a/android/vm_demo_native/main.cpp
+++ b/android/vm_demo_native/main.cpp
@@ -226,8 +226,10 @@
     ScopedFileDescriptor console_out_fd(fcntl(fileno(stdout), F_DUPFD_CLOEXEC));
     ScopedFileDescriptor console_in_fd(fcntl(fileno(stdin), F_DUPFD_CLOEXEC));
     ScopedFileDescriptor log_fd(fcntl(fileno(stdout), F_DUPFD_CLOEXEC));
+    ScopedFileDescriptor dump_dt_fd(-1);
 
-    ScopedAStatus ret = service.createVm(config, console_out_fd, console_in_fd, log_fd, &vm);
+    ScopedAStatus ret =
+            service.createVm(config, console_out_fd, console_in_fd, log_fd, dump_dt_fd, &vm);
     if (!ret.isOk()) {
         return Error() << "Failed to create VM";
     }
diff --git a/build/apex/Android.bp b/build/apex/Android.bp
index 4916df7..f794239 100644
--- a/build/apex/Android.bp
+++ b/build/apex/Android.bp
@@ -80,6 +80,11 @@
     }),
 }
 
+vintf_fragment {
+    name: "virtualizationservice.xml",
+    src: "virtualizationservice.xml",
+}
+
 apex_defaults {
     name: "com.android.virt_avf_enabled",
 
@@ -166,7 +171,7 @@
         true: "AndroidManifest.xml",
         default: unset,
     }),
-    vintf_fragments: select(soong_config_variable("ANDROID", "avf_remote_attestation_enabled"), {
+    vintf_fragment_modules: select(soong_config_variable("ANDROID", "avf_remote_attestation_enabled"), {
         "true": ["virtualizationservice.xml"],
         default: unset,
     }),
diff --git a/build/apex/product_packages.mk b/build/apex/product_packages.mk
index b2a4ca2..0646e67 100644
--- a/build/apex/product_packages.mk
+++ b/build/apex/product_packages.mk
@@ -26,11 +26,13 @@
     com.android.compos \
     features_com.android.virt.xml
 
-# TODO(b/207336449): Figure out how to get these off /system
+ifneq (true, $(RELEASE_INSTALL_APEX_SYSTEMSERVER_DEXPREOPT_SAME_PARTITION))
 PRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST := \
     system/framework/oat/%@service-compos.jar@classes.odex \
     system/framework/oat/%@service-compos.jar@classes.vdex \
 
+endif
+
 PRODUCT_APEX_SYSTEM_SERVER_JARS := com.android.compos:service-compos
 
 PRODUCT_SYSTEM_EXT_PROPERTIES := ro.config.isolated_compilation_enabled=true
@@ -71,7 +73,3 @@
     $(error RELEASE_AVF_ENABLE_EARLY_VM can only be enabled in trunk_staging until b/357025924 is fixed)
   endif
 endif
-
-ifdef RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES
-  PRODUCT_PACKAGES += LinuxInstallerAppStub
-endif
diff --git a/build/debian/build.sh b/build/debian/build.sh
index 2177b17..b4e8b2f 100755
--- a/build/debian/build.sh
+++ b/build/debian/build.sh
@@ -2,15 +2,15 @@
 
 # This is a script to build a Debian image that can run in a VM created via AVF.
 # TODOs:
-# - Support x86_64 architecture
 # - Add Android-specific packages via a new class
 # - Use a stable release from debian-cloud-images
 
 show_help() {
-	echo Usage: $0 [OPTION]... [FILE]
-	echo Builds a debian image and save it to FILE.
-	echo Options:
-	echo -h         Pring usage and this help message and exit.
+	echo "Usage: sudo $0 [OPTION]... [FILE]"
+	echo "Builds a debian image and save it to FILE. [sudo is required]"
+	echo "Options:"
+	echo "-h         Print usage and this help message and exit."
+	echo "-a ARCH    Architecture of the image [default is aarch64]"
 }
 
 check_sudo() {
@@ -21,83 +21,132 @@
 }
 
 parse_options() {
-	while getopts ":h" option; do
+	while getopts "ha:" option; do
 		case ${option} in
 			h)
 				show_help
 				exit;;
+			a)
+				if [[ "$OPTARG" != "aarch64" && "$OPTARG" != "x86_64" ]]; then
+					echo "Invalid architecture: $OPTARG"
+					exit
+				fi
+				arch="$OPTARG"
+				if [[ "$arch" == "x86_64" ]]; then
+					debian_arch="amd64"
+				fi
+				;;
+			*)
+				echo "Invalid option: $OPTARG"
+				exit
+				;;
 		esac
 	done
-	if [ -n "$1" ]; then
-		built_image=$1
+	if [[ "${*:$OPTIND:1}" ]]; then
+		built_image="${*:$OPTIND:1}"
 	fi
 }
 
 install_prerequisites() {
 	apt update
+	packages=(
+		binfmt-support
+		build-essential
+		ca-certificates
+		curl
+		debsums
+		dosfstools
+		fai-server
+		fai-setup-storage
+		fdisk
+		make
+		protobuf-compiler
+		python3
+		python3-libcloud
+		python3-marshmallow
+		python3-pytest
+		python3-yaml
+		qemu-user-static
+		qemu-utils
+		sudo
+		udev
+	)
+	if [[ "$arch" == "aarch64" ]]; then
+		packages+=(
+			gcc-aarch64-linux-gnu
+			libc6-dev-arm64-cross
+			qemu-system-arm
+		)
+	else
+		packages+=(
+			qemu-system
+		)
+	fi
+
+	# TODO(b/365955006): remove these lines when uboot supports x86_64 EFI application
+	if [[ "$arch" == "x86_64" ]]; then
+		packages+=(
+			libguestfs-tools
+		)
+	fi
 	DEBIAN_FRONTEND=noninteractive \
-	apt install --no-install-recommends --assume-yes \
-		binfmt-support \
-		ca-certificates \
-		debsums \
-		dosfstools \
-		fai-server \
-		fai-setup-storage \
-		fdisk \
-		make \
-		python3 \
-		python3-libcloud \
-		python3-marshmallow \
-		python3-pytest \
-		python3-yaml \
-		qemu-system-arm \
-		qemu-user-static \
-		qemu-utils \
-		udev \
+	apt install --no-install-recommends --assume-yes "${packages[@]}"
 
+	if [ ! -f $"HOME"/.cargo/bin/cargo ]; then
+		curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
+	fi
 
-        sed -i s/losetup\ -f/losetup\ -P\ -f/g /usr/sbin/fai-diskimage
-        sed -i 's/wget \$/wget -t 0 \$/g' /usr/share/debootstrap/functions
-
-        apt install --no-install-recommends --assume-yes curl
-        # just for testing
-        echo libseccomp: $(curl -is https://deb.debian.org/debian/pool/main/libs/libseccomp/libseccomp2_2.5.4-1+deb12u1_arm64.deb | head -n 1)
-        echo libsemanage-common: $(curl -is https://deb.debian.org/debian/pool/main/libs/libsemanage/libsemanage-common_3.4-1_all.deb | head -n 1)
-        echo libpcre2: $(curl -is https://deb.debian.org/debian/pool/main/p/pcre2/libpcre2-8-0_10.42-1_arm64.deb | head -n 1)
+	source "$HOME"/.cargo/env
+	rustup target add "${arch}"-unknown-linux-gnu
 }
 
 download_debian_cloud_image() {
 	local ver=master
 	local prj=debian-cloud-images
-	local url=https://salsa.debian.org/cloud-team/${prj}/-/archive/${ver}/${prj}-${ver}.tar.gz
-	local outdir=${debian_cloud_image}
+	local url="https://salsa.debian.org/cloud-team/${prj}/-/archive/${ver}/${prj}-${ver}.tar.gz"
+	local outdir="${debian_cloud_image}"
 
-	mkdir -p ${outdir}
-	wget -O - ${url} | tar xz -C ${outdir} --strip-components=1
+	mkdir -p "${outdir}"
+	wget -O - "${url}" | tar xz -C "${outdir}" --strip-components=1
+}
+
+build_rust_binary_and_copy() {
+	pushd "$(dirname "$0")/../../guest/$1" > /dev/null
+	RUSTFLAGS="-C linker=${arch}-linux-gnu-gcc" cargo build \
+		--target "${arch}-unknown-linux-gnu" \
+		--target-dir "${workdir}/$1"
+	mkdir -p "${dst}/files/usr/local/bin/$1"
+	cp "${workdir}/$1/${arch}-unknown-linux-gnu/debug/$1" "${dst}/files/usr/local/bin/$1/AVF"
+	chmod 777 "${dst}/files/usr/local/bin/$1/AVF"
+	popd > /dev/null
 }
 
 copy_android_config() {
-	local src=$(dirname $0)/fai_config
-	local dst=${config_space}
+	local src="$(dirname "$0")/fai_config"
+	local dst="${config_space}"
 
-	cp -R ${src}/* ${dst}
-	cp $(dirname $0)/image.yaml ${resources_dir}
+	cp -R "${src}"/* "${dst}"
+	cp "$(dirname "$0")/image.yaml" "${resources_dir}"
 
 	local ttyd_version=1.7.7
-	local url=https://github.com/tsl0922/ttyd/releases/download/${ttyd_version}/ttyd.aarch64
-	mkdir -p ${dst}/files/usr/local/bin/ttyd
-	wget ${url} -O ${dst}/files/usr/local/bin/ttyd/AVF
-	chmod 777 ${dst}/files/usr/local/bin/ttyd/AVF
+	local url="https://github.com/tsl0922/ttyd/releases/download/${ttyd_version}/ttyd.${arch}"
+	mkdir -p "${dst}/files/usr/local/bin/ttyd"
+	wget "${url}" -O "${dst}/files/usr/local/bin/ttyd/AVF"
+	chmod 777 "${dst}/files/usr/local/bin/ttyd/AVF"
+
+	build_rust_binary_and_copy forwarder_guest
+	build_rust_binary_and_copy forwarder_guest_launcher
+	build_rust_binary_and_copy ip_addr_reporter
 }
 
 run_fai() {
-	local out=${built_image}
-	make -C ${debian_cloud_image} image_bookworm_nocloud_arm64
-	mv ${debian_cloud_image}/image_bookworm_nocloud_arm64.raw ${out}
+	local out="${built_image}"
+	make -C "${debian_cloud_image}" "image_bookworm_nocloud_${debian_arch}"
+	mv "${debian_cloud_image}/image_bookworm_nocloud_${debian_arch}.raw" "${out}"
 }
 
 clean_up() {
-	rm -rf ${workdir}
+	rm -rf "${workdir}"
 }
 
 set -e
@@ -109,10 +158,27 @@
 debian_version=bookworm
 config_space=${debian_cloud_image}/config_space/${debian_version}
 resources_dir=${debian_cloud_image}/src/debian_cloud_images/resources
+arch=aarch64
+debian_arch=arm64
+parse_options "$@"
 check_sudo
-parse_options $@
 install_prerequisites
 download_debian_cloud_image
 copy_android_config
 run_fai
 fdisk -l image.raw
+images=(image.raw)
+# 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+=(
+		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
diff --git a/build/debian/build_in_container.sh b/build/debian/build_in_container.sh
new file mode 100755
index 0000000..fd1a975
--- /dev/null
+++ b/build/debian/build_in_container.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+if [ -z "$ANDROID_BUILD_TOP" ]; then echo "forgot to source build/envsetup.sh?" && exit 1; fi
+
+arch=aarch64
+while getopts "a:" option; do
+  case ${option} in
+    a)
+      if [[ "$OPTARG" != "aarch64" && "$OPTARG" != "x86_64" ]]; then
+        echo "Invalid architecture: $OPTARG"
+        exit
+      fi
+      arch="$OPTARG"
+      ;;
+    *)
+      echo "Invalid option: $OPTARG"
+      exit
+      ;;
+  esac
+done
+
+docker run --privileged -it --workdir /root/Virtualization/build/debian -v \
+  "$ANDROID_BUILD_TOP/packages/modules/Virtualization:/root/Virtualization" -v \
+  /dev:/dev ubuntu:22.04 /root/Virtualization/build/debian/build.sh -a "$arch"
diff --git a/build/debian/fai_config/files/etc/systemd/system/ip_addr_reporter.service/AVF b/build/debian/fai_config/files/etc/systemd/system/ip_addr_reporter.service/AVF
new file mode 100644
index 0000000..7d163fb
--- /dev/null
+++ b/build/debian/fai_config/files/etc/systemd/system/ip_addr_reporter.service/AVF
@@ -0,0 +1,13 @@
+[Unit]
+Description=ip report service
+After=syslog.target
+After=network.target
+Requires=ttyd.service
+[Service]
+ExecStart=/usr/local/bin/ip_addr_reporter
+Type=simple
+Restart=on-failure
+User=root
+Group=root
+[Install]
+WantedBy=multi-user.target
diff --git a/build/debian/fai_config/files/etc/systemd/system/ttyd.service/AVF b/build/debian/fai_config/files/etc/systemd/system/ttyd.service/AVF
index f71557d..0aab770 100644
--- a/build/debian/fai_config/files/etc/systemd/system/ttyd.service/AVF
+++ b/build/debian/fai_config/files/etc/systemd/system/ttyd.service/AVF
@@ -3,7 +3,7 @@
 After=syslog.target
 After=network.target
 [Service]
-ExecStart=/usr/local/bin/ttyd -W screen -aAxR -S main login
+ExecStart=/usr/local/bin/ttyd --ssl --ssl-cert /etc/ttyd/server.crt --ssl-key /etc/ttyd/server.key --ssl-ca /etc/ttyd/ca.crt -W screen -aAxR -S main login -f droid
 Type=simple
 Restart=always
 User=root
diff --git a/build/debian/fai_config/files/etc/systemd/system/vsockip.service/AVF b/build/debian/fai_config/files/etc/systemd/system/vsockip.service/AVF
deleted file mode 100644
index a29020b..0000000
--- a/build/debian/fai_config/files/etc/systemd/system/vsockip.service/AVF
+++ /dev/null
@@ -1,12 +0,0 @@
-[Unit]
-Description=vsock ip service
-After=syslog.target
-After=network.target
-[Service]
-ExecStart=/usr/bin/python3 /usr/local/bin/vsock.py
-Type=simple
-Restart=always
-User=root
-Group=root
-[Install]
-WantedBy=multi-user.target
diff --git a/build/debian/fai_config/files/etc/ttyd/ca.crt/AVF b/build/debian/fai_config/files/etc/ttyd/ca.crt/AVF
new file mode 100644
index 0000000..90d8c0e
--- /dev/null
+++ b/build/debian/fai_config/files/etc/ttyd/ca.crt/AVF
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDhzCCAm+gAwIBAgIUQkvURjf6sU5aJ7oK9usHnJHsc/owDQYJKoZIhvcNAQEL
+BQAwUzELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkdEMQswCQYDVQQHDAJTWjETMBEG
+A1UECgwKQWNtZSwgSW5jLjEVMBMGA1UEAwwMQWNtZSBSb290IENBMB4XDTI0MTAx
+NDAxMjgzN1oXDTI1MTAxNDAxMjgzN1owUzELMAkGA1UEBhMCQ04xCzAJBgNVBAgM
+AkdEMQswCQYDVQQHDAJTWjETMBEGA1UECgwKQWNtZSwgSW5jLjEVMBMGA1UEAwwM
+QWNtZSBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtjgS
+ePtWI6xARLzM1bUMvqtWwY4ci4TzcOcfLfV5Eqbb135NSBKQ+Q2IAguc2Bl3ZVRE
+08GhQ9XJOo+mp2SUY/8+SJpCVhVlWvF6LwXd8X5pZ9GCem0FXY02kMr5ZiTs/CN2
+LZIyJKgXCT/5208on+BbiNp0pk2Pz1nDOdpxvkDJ8UKRWLwqCAEM/rcN1Lc00aln
+N/Rfi/CQE+MDAmhuy/nxr37ldqhkN+xM4bhNs1bjyVposKtbmFUY/SD3ca5CMawU
+E3l5hZ5kfua7lelEPVhvNYJcxffVO0fPNEbUKr1WsPLrnidqegcU8bml1BoCphgA
+qzoxD0rZniqMsom/vwIDAQABo1MwUTAdBgNVHQ4EFgQUZOHF7/arn8ODqEj1Wifk
+dEA5TFkwHwYDVR0jBBgwFoAUZOHF7/arn8ODqEj1WifkdEA5TFkwDwYDVR0TAQH/
+BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAVets3IybnIycAtajxpJygdji/95t
+ikdyWbi8lrszC0E5bCR9XPQKnqx/svKYrEVQNihH/nZ6TlTv0f3b77+92sVlmQfl
+a3KKI6qIgcqNEO2lHYsS+cPeBmaM6WXcEPe6gEnan1i5N16B9g9ntY4lOg8Z4roR
+2lVVCCNwabyBxb5oQDsN1IDeJ7JRRZqGGduDSZTvdd36GqNhMvXQjluyJCCFd1Hv
+IwwJmAR2GMUQU8Eoa+zGzW1Inf1YJytTu8SeQ+hYy2QCG88vZigJdifmhETDDz9Q
+xQjp1SCNIBxFHY2voqtiJtfupN5pVieECZS42pbVHMIAUOk7BmNcEWnSKw==
+-----END CERTIFICATE-----
diff --git a/build/debian/fai_config/files/etc/ttyd/server.crt/AVF b/build/debian/fai_config/files/etc/ttyd/server.crt/AVF
new file mode 100644
index 0000000..b4ca829
--- /dev/null
+++ b/build/debian/fai_config/files/etc/ttyd/server.crt/AVF
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDiTCCAnGgAwIBAgIUasfD1K/4tJHwNRXL2kdSD9VbeSwwDQYJKoZIhvcNAQEL
+BQAwUzELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkdEMQswCQYDVQQHDAJTWjETMBEG
+A1UECgwKQWNtZSwgSW5jLjEVMBMGA1UEAwwMQWNtZSBSb290IENBMB4XDTI0MTAx
+NDAxMjgzOFoXDTI1MTAxNDAxMjgzOFowUDELMAkGA1UEBhMCQ04xCzAJBgNVBAgM
+AkdEMQswCQYDVQQHDAJTWjETMBEGA1UECgwKQWNtZSwgSW5jLjESMBAGA1UEAwwJ
+bG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3+uVF4TP
+jUjfL8vJlECAN1rLFK8lDuOUv52VCrW7MXMfGYlA4nk1OKDjygnZIpET6I9cTfCG
+Xiwad6bU6Oqy4MZ2i338F+eERrGpkitSQ7QRqZannjBIDFxXZvJpMTJDIWNCmz+P
+K2VcvCh8im2tJA66wJogUcVmJBugNqleqxFcxPvXOdBdWBK7JYOcb4J643eLX6+D
+X6v2QTlKXfihouVC8wAzbw9HHmOVb7ono1rV7xpcFrOyBiDGVSgEteiB8l26iXA9
+fExkb0rUzHjlgvb/l8/nGAaQHd0eE+/SGd4tXvs9KHX6XJh/PI0ExTsDIBDcuVOt
+2YzXeuM6zzrKLQIDAQABo1gwVjAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0O
+BBYEFHpFYqFC/AEOfWfdZmpy5YBZfgR2MB8GA1UdIwQYMBaAFGThxe/2q5/Dg6hI
+9Von5HRAOUxZMA0GCSqGSIb3DQEBCwUAA4IBAQBQspP3wo3yzcPWuFk4lRyo7zpF
+JfBBX0UU1Z0MQfIGxLC2YtRvxobRqwLcKUKQjBqUuRdukleOaVVFeXb/HI9vY3ji
+9PfUb2UJ4O3z3pdSK0EwXbkCidtUflRLvPG6dgBrXyLOqxBqA5lWR2ds5HRAMRAi
+eXfDkJTmNOAQAnPgM+35FBgmhh6axG+bUudvvVoA8ca+zW9i1R6/vblxYJ6bhmw0
+8s+uoAX6FXcZ0YFOGdhcpJmnbiRd3D0VVacjc3b9pjFOI8d3bh9pR47p0kVOaRsh
+aAG3gZhyMPOgbYceCjfzND5YhycDI+MzPo/JOYdhHGGJawoh1nP94QNPan6J
+-----END CERTIFICATE-----
diff --git a/build/debian/fai_config/files/etc/ttyd/server.key/AVF b/build/debian/fai_config/files/etc/ttyd/server.key/AVF
new file mode 100644
index 0000000..37933b2
--- /dev/null
+++ b/build/debian/fai_config/files/etc/ttyd/server.key/AVF
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDf65UXhM+NSN8v
+y8mUQIA3WssUryUO45S/nZUKtbsxcx8ZiUDieTU4oOPKCdkikRPoj1xN8IZeLBp3
+ptTo6rLgxnaLffwX54RGsamSK1JDtBGplqeeMEgMXFdm8mkxMkMhY0KbP48rZVy8
+KHyKba0kDrrAmiBRxWYkG6A2qV6rEVzE+9c50F1YErslg5xvgnrjd4tfr4Nfq/ZB
+OUpd+KGi5ULzADNvD0ceY5VvuiejWtXvGlwWs7IGIMZVKAS16IHyXbqJcD18TGRv
+StTMeOWC9v+Xz+cYBpAd3R4T79IZ3i1e+z0odfpcmH88jQTFOwMgENy5U63ZjNd6
+4zrPOsotAgMBAAECggEAARJYlD12ch5WM2aDrPOGOAtREOfP7CCwWcMiOfBP72iR
+Y9Vipxmuz16nwTJ22F7HvPsdPOUo1cFtWhim2Aqr/ZxuT4Ce9oVrk6iDwRdeuYdY
+cIhtChvJi+p0ggMcuyzp90+3AYXxynsOlCufMjSNGaqvYUsNEXnJFSgiKr7mgbIO
+J0VU1Wrquw7N58RKL+T3xEvE7uO3QpLOim2MbfRSVq/JGNxqAGw0/uxtjFs7Vtf9
+z44e/ULeYDS7zMj6cMggxQp5nfzcboGoNVUEDgYjOzqXCe4cG0n3XfN7GJhaS1ZF
+tPd8l4Ch0IrT4hs5uVFaMdFbj+er7mvmqfTVytrRmQKBgQD2kVB53EKhqxgvz2N7
+bAJglOLd6FWKsWlLMSdER0/4dRVRMIBxnYWgQ0gaRc4TM7oyKOl3MDF9jdDne5KJ
+cnfzFoH2GD6VBQRr0mFmV1UV6oHEjDJBasMo/1Vw3TJ4oZgZpYpJjrDmPWZqHUs4
+I79TdvJrNFSmk3MGVFjatLIq5QKBgQDofHpHfBeRCn2Z3OOkiAN5V53n5deZl6Jt
+lGTsrXKpEzRTre4LWZojoB9hiGjptZkXHA2HW90RiV9OHhTa8W9ZntLnOnWc5RUn
+Tzh14KupjsBQm/gE8SuqHSDx1mxTnIUo0W28d/Beecri5KfaoEY+wxZXOeQy5JFR
+ec/AhU4FqQKBgGhVzUwDnF502+M/SsVrSwY7elSUf74UnI2o2wjVdE2anc6hS3jI
+Q0cxsU0MxMrzVJLtJP2+cvLCE+ggLj3jJkbC+3N7ht/gI6LMf1KjGeoQNaFKAeoU
+l0i94xXDRBwvpQEVP5MowkprKO82PiIfXlKfPq2Gk1t5gW7oOkExvULRAoGBAK7R
+051nec0uJ06I5IE3ae1X7jyP//TWKmTeHpo+vybWcxWth3/va9H4OUC9M67ySGEx
+ThcIBA+IzirOwf31aTbqEEuiEQje1m5NyvYQ8OS6nHDBJ9qHg78S0lAoXiLtYtBT
+04HSauSQDvlY2cOzm77cMjN7K9b9Oy0aPRfW5dmpAoGAGesq4Ojky4crpi0H1O7n
+cMuIAzaPozsMx7iSrhUe69fwVFiMkEKR6ems01DmjYwPb6DtxCieaRlGbd9E8oIZ
+y6n+Uh9Qbc5sDhPMsys6NyKOv/A6rkn49/etr40f0Z5g9g/d2+qtwoAXjo3sSPuW
+7iqbruRjbKUaJKzdpIqOKD0=
+-----END PRIVATE KEY-----
diff --git a/build/debian/fai_config/files/usr/local/bin/vsock.py/AVF b/build/debian/fai_config/files/usr/local/bin/vsock.py/AVF
deleted file mode 100755
index 292d953..0000000
--- a/build/debian/fai_config/files/usr/local/bin/vsock.py/AVF
+++ /dev/null
@@ -1,57 +0,0 @@
-#!/usr/bin/env python3
-
-import socket
-
-# Constants for vsock (from linux/vm_sockets.h)
-AF_VSOCK = 40
-SOCK_STREAM = 1
-VMADDR_CID_ANY = -1
-
-def get_local_ip():
-    """Retrieves the first IPv4 address found on the system.
-
-    Returns:
-        str: The local IPv4 address, or '127.0.0.1' if no IPv4 address is found.
-    """
-
-    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-    try:
-        s.connect(('8.8.8.8', 80))
-        ip = s.getsockname()[0]
-    except Exception:
-        ip = '127.0.0.1'
-    finally:
-        s.close()
-    return ip
-
-def main():
-    PORT = 1024
-
-    # Create a vsock socket
-    server_socket = socket.socket(AF_VSOCK, SOCK_STREAM)
-
-    # Bind the socket to the server address
-    server_address = (VMADDR_CID_ANY, PORT)
-    server_socket.bind(server_address)
-
-    # Listen for incoming connections
-    server_socket.listen(1)
-    print(f"VSOCK server listening on port {PORT}...")
-
-    while True:
-        # Accept a connection
-        connection, client_address = server_socket.accept()
-        print(f"Connection from: {client_address}")
-
-        try:
-            # Get the local IP address
-            local_ip = get_local_ip()
-
-            # Send the IP address to the client
-            connection.sendall(local_ip.encode())
-        finally:
-            # Close the connection
-            connection.close()
-
-if __name__ == "__main__":
-    main()
diff --git a/build/debian/fai_config/scripts/AVF/10-systemd b/build/debian/fai_config/scripts/AVF/10-systemd
index e04a562..6a106c6 100755
--- a/build/debian/fai_config/scripts/AVF/10-systemd
+++ b/build/debian/fai_config/scripts/AVF/10-systemd
@@ -1,6 +1,8 @@
 #!/bin/bash
 
+chmod +x $target/usr/local/bin/forwarder_guest
+chmod +x $target/usr/local/bin/forwarder_guest_launcher
+chmod +x $target/usr/local/bin/ip_addr_reporter
 chmod +x $target/usr/local/bin/ttyd
-chmod +x $target/usr/local/bin/vsock.py
 ln -s /etc/systemd/system/ttyd.service $target/etc/systemd/system/multi-user.target.wants/ttyd.service
-ln -s /etc/systemd/system/vsockip.service $target/etc/systemd/system/multi-user.target.wants/vsockip.service
\ No newline at end of file
+ln -s /etc/systemd/system/ip_addr_reporter.service $target/etc/systemd/system/multi-user.target.wants/ip_addr_reporter.service
diff --git a/build/debian/fai_config/scripts/AVF/20-useradd b/build/debian/fai_config/scripts/AVF/20-useradd
new file mode 100755
index 0000000..9fbcd43
--- /dev/null
+++ b/build/debian/fai_config/scripts/AVF/20-useradd
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+$ROOTCMD useradd -m -u 1000 -N -G sudo droid
+$ROOTCMD echo 'droid ALL=(ALL) NOPASSWD:ALL' >> $target/etc/sudoers
\ No newline at end of file
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh b/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh
new file mode 100644
index 0000000..7a1523a
--- /dev/null
+++ b/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+set -e
+
+cd "${KOKORO_ARTIFACTS_DIR}/git/avf/build/debian/"
+sudo losetup -D
+grep vmx /proc/cpuinfo || true
+sudo ./build.sh
+sudo mv images.tar.gz ${KOKORO_ARTIFACTS_DIR} || true
+
+mkdir -p ${KOKORO_ARTIFACTS_DIR}/logs
+sudo cp -r /var/log/fai/* ${KOKORO_ARTIFACTS_DIR}/logs || true
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg b/build/debian/kokoro/gcp_ubuntu_docker/aarch64/continuous.cfg
similarity index 73%
rename from build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg
rename to build/debian/kokoro/gcp_ubuntu_docker/aarch64/continuous.cfg
index 111096d..e836eea 100644
--- a/build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg
+++ b/build/debian/kokoro/gcp_ubuntu_docker/aarch64/continuous.cfg
@@ -4,10 +4,11 @@
 
 # Location of the bash script. Should have value <git_on_borg_scm.name>/<path_from_repository_root>.
 # git_on_borg_scm.name is specified in the job configuration (next section).
-build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/build.sh"
+build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh"
 
 action {
   define_artifacts {
-    regex: "image.raw"
+    regex: "images.tar.gz"
+    regex: "logs/**"
   }
 }
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/hourly.cfg b/build/debian/kokoro/gcp_ubuntu_docker/aarch64/hourly.cfg
similarity index 73%
rename from build/debian/kokoro/gcp_ubuntu_docker/hourly.cfg
rename to build/debian/kokoro/gcp_ubuntu_docker/aarch64/hourly.cfg
index 111096d..e836eea 100644
--- a/build/debian/kokoro/gcp_ubuntu_docker/hourly.cfg
+++ b/build/debian/kokoro/gcp_ubuntu_docker/aarch64/hourly.cfg
@@ -4,10 +4,11 @@
 
 # Location of the bash script. Should have value <git_on_borg_scm.name>/<path_from_repository_root>.
 # git_on_borg_scm.name is specified in the job configuration (next section).
-build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/build.sh"
+build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh"
 
 action {
   define_artifacts {
-    regex: "image.raw"
+    regex: "images.tar.gz"
+    regex: "logs/**"
   }
 }
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg b/build/debian/kokoro/gcp_ubuntu_docker/aarch64/presubmit.cfg
similarity index 72%
copy from build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg
copy to build/debian/kokoro/gcp_ubuntu_docker/aarch64/presubmit.cfg
index 111096d..e836eea 100644
--- a/build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg
+++ b/build/debian/kokoro/gcp_ubuntu_docker/aarch64/presubmit.cfg
@@ -4,10 +4,11 @@
 
 # Location of the bash script. Should have value <git_on_borg_scm.name>/<path_from_repository_root>.
 # git_on_borg_scm.name is specified in the job configuration (next section).
-build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/build.sh"
+build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh"
 
 action {
   define_artifacts {
-    regex: "image.raw"
+    regex: "images.tar.gz"
+    regex: "logs/**"
   }
 }
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/build.sh b/build/debian/kokoro/gcp_ubuntu_docker/build.sh
deleted file mode 100644
index 4598d1c..0000000
--- a/build/debian/kokoro/gcp_ubuntu_docker/build.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-
-set -e
-
-cd "${KOKORO_ARTIFACTS_DIR}/git/avf/build/debian/"
-sudo losetup -D
-grep vmx /proc/cpuinfo || true
-sudo ./build.sh
-cp image.raw ${KOKORO_ARTIFACTS_DIR}
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/x86_64/build.sh b/build/debian/kokoro/gcp_ubuntu_docker/x86_64/build.sh
new file mode 100644
index 0000000..66e3d64
--- /dev/null
+++ b/build/debian/kokoro/gcp_ubuntu_docker/x86_64/build.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+set -e
+
+cd "${KOKORO_ARTIFACTS_DIR}/git/avf/build/debian/"
+sudo losetup -D
+grep vmx /proc/cpuinfo || true
+sudo ./build.sh -a x86_64
+sudo mv images.tar.gz ${KOKORO_ARTIFACTS_DIR} || true
+
+mkdir -p ${KOKORO_ARTIFACTS_DIR}/logs
+sudo cp -r /var/log/fai/* ${KOKORO_ARTIFACTS_DIR}/logs || true
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg b/build/debian/kokoro/gcp_ubuntu_docker/x86_64/continuous.cfg
similarity index 73%
copy from build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg
copy to build/debian/kokoro/gcp_ubuntu_docker/x86_64/continuous.cfg
index 111096d..a5e8aeb 100644
--- a/build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg
+++ b/build/debian/kokoro/gcp_ubuntu_docker/x86_64/continuous.cfg
@@ -4,10 +4,11 @@
 
 # Location of the bash script. Should have value <git_on_borg_scm.name>/<path_from_repository_root>.
 # git_on_borg_scm.name is specified in the job configuration (next section).
-build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/build.sh"
+build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/x86_64/build.sh"
 
 action {
   define_artifacts {
-    regex: "image.raw"
+    regex: "images.tar.gz"
+    regex: "logs/**"
   }
 }
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/hourly.cfg b/build/debian/kokoro/gcp_ubuntu_docker/x86_64/hourly.cfg
similarity index 73%
copy from build/debian/kokoro/gcp_ubuntu_docker/hourly.cfg
copy to build/debian/kokoro/gcp_ubuntu_docker/x86_64/hourly.cfg
index 111096d..a5e8aeb 100644
--- a/build/debian/kokoro/gcp_ubuntu_docker/hourly.cfg
+++ b/build/debian/kokoro/gcp_ubuntu_docker/x86_64/hourly.cfg
@@ -4,10 +4,11 @@
 
 # Location of the bash script. Should have value <git_on_borg_scm.name>/<path_from_repository_root>.
 # git_on_borg_scm.name is specified in the job configuration (next section).
-build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/build.sh"
+build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/x86_64/build.sh"
 
 action {
   define_artifacts {
-    regex: "image.raw"
+    regex: "images.tar.gz"
+    regex: "logs/**"
   }
 }
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg b/build/debian/kokoro/gcp_ubuntu_docker/x86_64/presubmit.cfg
similarity index 72%
copy from build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg
copy to build/debian/kokoro/gcp_ubuntu_docker/x86_64/presubmit.cfg
index 111096d..a5e8aeb 100644
--- a/build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg
+++ b/build/debian/kokoro/gcp_ubuntu_docker/x86_64/presubmit.cfg
@@ -4,10 +4,11 @@
 
 # Location of the bash script. Should have value <git_on_borg_scm.name>/<path_from_repository_root>.
 # git_on_borg_scm.name is specified in the job configuration (next section).
-build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/build.sh"
+build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/x86_64/build.sh"
 
 action {
   define_artifacts {
-    regex: "image.raw"
+    regex: "images.tar.gz"
+    regex: "logs/**"
   }
 }
diff --git a/android/LinuxInstaller/linux_image_builder/vm_config.json b/build/debian/vm_config.json.aarch64
similarity index 87%
rename from android/LinuxInstaller/linux_image_builder/vm_config.json
rename to build/debian/vm_config.json.aarch64
index 21462b8..9f9295c 100644
--- a/android/LinuxInstaller/linux_image_builder/vm_config.json
+++ b/build/debian/vm_config.json.aarch64
@@ -2,7 +2,7 @@
     "name": "debian",
     "disks": [
         {
-            "image": "/data/local/tmp/debian.img",
+            "image": "$PAYLOAD_DIR/image.raw",
             "partitions": [],
             "writable": true
         }
diff --git a/android/LinuxInstaller/linux_image_builder/vm_config.json b/build/debian/vm_config.json.x86_64
similarity index 69%
copy from android/LinuxInstaller/linux_image_builder/vm_config.json
copy to build/debian/vm_config.json.x86_64
index 21462b8..2fb9faa 100644
--- a/android/LinuxInstaller/linux_image_builder/vm_config.json
+++ b/build/debian/vm_config.json.x86_64
@@ -2,11 +2,14 @@
     "name": "debian",
     "disks": [
         {
-            "image": "/data/local/tmp/debian.img",
+            "image": "$PAYLOAD_DIR/image.raw",
             "partitions": [],
             "writable": true
         }
     ],
+    "kernel": "$PAYLOAD_DIR/vmlinuz",
+    "initrd": "$PAYLOAD_DIR/initrd.img",
+    "params": "root=/dev/vda1",
     "protected": false,
     "cpu_topology": "match_host",
     "platform_version": "~1.0",
diff --git a/docs/vm_remote_attestation.md b/docs/vm_remote_attestation.md
index ee20591..2ee0fae 100644
--- a/docs/vm_remote_attestation.md
+++ b/docs/vm_remote_attestation.md
@@ -126,7 +126,7 @@
 
 To support VM remote attestation, vendors must include an RKP VM marker in their
 DICE certificates. This marker should be present from the early boot stage
-within the TEE and continue through to the last DICE certificate before
+within the TEE and continue through to the leaf DICE certificate before
 [pvmfw][pvmfw] takes over.
 
 ![RKP VM DICE chain][rkpvm-dice-chain]
@@ -140,6 +140,20 @@
 server because it will lack the RKP VM marker that pvmfw would have added in a
 genuine RKP VM boot process.
 
+### Testing
+
+To ensure the correct implementation and usage of RKP VM markers, we've
+incorporated comprehensive checks into various xTS tests (e.g.,
+`VtsHalRemotelyProvisionedComponentTargetTest`).
+
+These tests validate the following conditions:
+
+- The RKP VM DICE chain must have a continuous presence of at least two RKP VM
+  markers, extending to the leaf DICE certificate.
+- Non-RKP VM DICE chains must not have a continuous presence of two or more RKP
+  VM markers, preventing non-RKP VM chains from being incorrectly identified as
+  RKP VM chains.
+
 [pvmfw]: ../guest/pvmfw/README.md
 [rkpvm-dice-chain]: img/rkpvm-dice-chain.png
 
diff --git a/build/debian/forwarder_guest/Cargo.toml b/guest/forwarder_guest/Cargo.toml
similarity index 79%
rename from build/debian/forwarder_guest/Cargo.toml
rename to guest/forwarder_guest/Cargo.toml
index e70dcd4..65f57cf 100644
--- a/build/debian/forwarder_guest/Cargo.toml
+++ b/guest/forwarder_guest/Cargo.toml
@@ -5,7 +5,7 @@
 
 [dependencies]
 clap = { version = "4.5.19", features = ["derive"] }
-forwarder = { path = "../../../libs/libforwarder" }
+forwarder = { path = "../../libs/libforwarder" }
 poll_token_derive = "0.1.0"
 remain = "0.2.14"
 vmm-sys-util = "0.12.1"
diff --git a/build/debian/forwarder_guest/src/main.rs b/guest/forwarder_guest/src/main.rs
similarity index 100%
rename from build/debian/forwarder_guest/src/main.rs
rename to guest/forwarder_guest/src/main.rs
diff --git a/guest/forwarder_guest_launcher/Cargo.toml b/guest/forwarder_guest_launcher/Cargo.toml
new file mode 100644
index 0000000..bf0c0ed
--- /dev/null
+++ b/guest/forwarder_guest_launcher/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "forwarder_guest_launcher"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+clap = { version = "4.5.20", features = ["derive"] }
+prost = "0.13.3"
+tokio = { version = "1.40.0", features = ["rt-multi-thread"] }
+tonic = "0.12.3"
+
+[build-dependencies]
+tonic-build = "0.12.3"
diff --git a/guest/forwarder_guest_launcher/build.rs b/guest/forwarder_guest_launcher/build.rs
new file mode 100644
index 0000000..c923747
--- /dev/null
+++ b/guest/forwarder_guest_launcher/build.rs
@@ -0,0 +1,18 @@
+// Copyright 2024 The Android Open Source Project
+//
+// 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.
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    tonic_build::compile_protos("../../libs/debian_service/proto/DebianService.proto")?;
+    Ok(())
+}
diff --git a/guest/forwarder_guest_launcher/src/main.rs b/guest/forwarder_guest_launcher/src/main.rs
new file mode 100644
index 0000000..4042fe5
--- /dev/null
+++ b/guest/forwarder_guest_launcher/src/main.rs
@@ -0,0 +1,50 @@
+// Copyright 2024 The Android Open Source Project
+//
+// 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.
+
+//! Launcher of forwarder_guest
+
+use clap::Parser;
+use debian_service::debian_service_client::DebianServiceClient;
+use debian_service::Empty;
+use tonic::transport::Endpoint;
+use tonic::Request;
+
+mod debian_service {
+    tonic::include_proto!("com.android.virtualization.vmlauncher.proto");
+}
+
+#[derive(Parser)]
+/// Flags for running command
+pub struct Args {
+    /// Host IP address
+    #[arg(long)]
+    #[arg(alias = "host")]
+    host_addr: String,
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let args = Args::parse();
+    let addr = format!("https://{}:12000", args.host_addr);
+
+    let channel = Endpoint::from_shared(addr)?.connect().await?;
+    let mut client = DebianServiceClient::new(channel);
+    let mut res_stream =
+        client.open_forwarding_request_queue(Request::new(Empty {})).await?.into_inner();
+
+    while let Some(response) = res_stream.message().await? {
+        println!("Response from the host: {:?}", response);
+    }
+    Ok(())
+}
diff --git a/guest/ip_addr_reporter/.gitignore b/guest/ip_addr_reporter/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/guest/ip_addr_reporter/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/guest/ip_addr_reporter/Cargo.toml b/guest/ip_addr_reporter/Cargo.toml
new file mode 100644
index 0000000..e255eaf
--- /dev/null
+++ b/guest/ip_addr_reporter/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "ip_addr_reporter"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+netdev = "0.31.0"
+prost = "0.13.3"
+tokio = { version = "1.40.0", features = ["rt-multi-thread"] }
+tonic = "0.12.3"
+
+[build-dependencies]
+tonic-build = "0.12.3"
diff --git a/guest/ip_addr_reporter/build.rs b/guest/ip_addr_reporter/build.rs
new file mode 100644
index 0000000..e3939d4
--- /dev/null
+++ b/guest/ip_addr_reporter/build.rs
@@ -0,0 +1,7 @@
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let proto_file = "../../libs/debian_service/proto/DebianService.proto";
+
+    tonic_build::compile_protos(proto_file).unwrap();
+
+    Ok(())
+}
diff --git a/guest/ip_addr_reporter/src/main.rs b/guest/ip_addr_reporter/src/main.rs
new file mode 100644
index 0000000..5784a83
--- /dev/null
+++ b/guest/ip_addr_reporter/src/main.rs
@@ -0,0 +1,26 @@
+use api::debian_service_client::DebianServiceClient;
+use api::IpAddr;
+
+pub mod api {
+    tonic::include_proto!("com.android.virtualization.vmlauncher.proto");
+}
+
+#[tokio::main]
+async fn main() -> Result<(), String> {
+    let gateway_ip_addr = netdev::get_default_gateway()?.ipv4[0];
+    let ip_addr = netdev::get_default_interface()?.ipv4[0].addr();
+    const PORT: i32 = 12000;
+
+    let server_addr = format!("http://{}:{}", gateway_ip_addr.to_string(), PORT);
+
+    println!("local ip addr: {}", ip_addr.to_string());
+    println!("coonect to grpc server {}", server_addr);
+
+    let mut client = DebianServiceClient::connect(server_addr).await.map_err(|e| e.to_string())?;
+
+    let request = tonic::Request::new(IpAddr { addr: ip_addr.to_string() });
+
+    let response = client.report_vm_ip_addr(request).await.map_err(|e| e.to_string())?;
+    println!("response from server: {:?}", response);
+    Ok(())
+}
diff --git a/guest/microdroid_manager/Android.bp b/guest/microdroid_manager/Android.bp
index 9c9a3d0..1824c20 100644
--- a/guest/microdroid_manager/Android.bp
+++ b/guest/microdroid_manager/Android.bp
@@ -43,7 +43,6 @@
         "libmicrodroid_payload_config",
         "libmicrodroid_uids",
         "libnix",
-        "libonce_cell",
         "libopenssl",
         "libprotobuf",
         "librpcbinder_rs",
diff --git a/guest/port_listener/build.sh b/guest/port_listener/build.sh
new file mode 100755
index 0000000..a1d0205
--- /dev/null
+++ b/guest/port_listener/build.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+
+set -e
+
+check_sudo() {
+	if [ "$EUID" -ne 0 ]; then
+		echo "Please run as root."
+		exit
+	fi
+}
+
+install_prerequisites() {
+    apt update
+    apt install --no-install-recommends --assume-yes \
+        bpftool \
+        clang \
+        libbpf-dev \
+        libgoogle-glog-dev \
+        libstdc++-14-dev
+}
+
+build_port_listener() {
+    cp $(dirname $0)/src/* ${workdir}
+    out_dir=${PWD}
+    pushd ${workdir}
+        bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
+        clang \
+            -O2 \
+            -Wall \
+            -target bpf \
+            -g \
+            -c listen_tracker.ebpf.c \
+            -o listen_tracker.ebpf.o
+        bpftool gen skeleton listen_tracker.ebpf.o > listen_tracker.skel.h
+        clang++ \
+            -O2 \
+            -Wall \
+            -lbpf \
+            -lglog \
+            -o port_listener \
+            main.cc
+        cp port_listener ${out_dir}
+    popd
+}
+
+clean_up() {
+	rm -rf ${workdir}
+}
+trap clean_up EXIT
+workdir=$(mktemp -d)
+
+check_sudo
+install_prerequisites
+build_port_listener
diff --git a/guest/port_listener/src/common.h b/guest/port_listener/src/common.h
new file mode 100644
index 0000000..d6e507c
--- /dev/null
+++ b/guest/port_listener/src/common.h
@@ -0,0 +1,31 @@
+// Copyright 2024 The Android Open Source Project
+//
+// 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.
+
+// Copied from ChromiumOS with relicensing:
+// src/platform2/vm_tools/port_listener/common.h
+
+#ifndef VM_TOOLS_PORT_LISTENER_COMMON_H_
+#define VM_TOOLS_PORT_LISTENER_COMMON_H_
+
+enum State {
+    kPortListenerUp,
+    kPortListenerDown,
+};
+
+struct event {
+    enum State state;
+    uint16_t port;
+};
+
+#endif // VM_TOOLS_PORT_LISTENER_COMMON_H_
diff --git a/guest/port_listener/src/listen_tracker.ebpf.c b/guest/port_listener/src/listen_tracker.ebpf.c
new file mode 100644
index 0000000..9e98aad
--- /dev/null
+++ b/guest/port_listener/src/listen_tracker.ebpf.c
@@ -0,0 +1,80 @@
+// Copyright 2024 The Android Open Source Project
+//
+// 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.
+
+// Copied from ChromiumOS with relicensing:
+// src/platform2/vm_tools/port_listener/listen_tracker.ebpf.c
+
+// bpf_helpers.h uses types defined here
+#include <bpf/bpf_helpers.h>
+
+#include "common.h"
+#include "vmlinux.h"
+
+// For some reason 6.1 doesn't include these symbols in the debug build
+// so they don't get included in vmlinux.h. These features have existed since
+// well before 6.1.
+#define BPF_F_NO_PREALLOC (1U << 0)
+#define BPF_ANY 0
+
+struct {
+    __uint(type, BPF_MAP_TYPE_RINGBUF);
+    __uint(max_entries, 1 << 24);
+} events SEC(".maps");
+
+struct {
+    __uint(type, BPF_MAP_TYPE_HASH);
+    __type(key, struct sock*);
+    __type(value, __u8);
+    __uint(max_entries, 65535);
+    __uint(map_flags, BPF_F_NO_PREALLOC);
+} sockmap SEC(".maps");
+
+const __u8 set_value = 0;
+
+SEC("tp/sock/inet_sock_set_state")
+int tracepoint_inet_sock_set_state(struct trace_event_raw_inet_sock_set_state* ctx) {
+    // We don't support anything other than TCP.
+    if (ctx->protocol != IPPROTO_TCP) {
+        return 0;
+    }
+    struct sock* sk = (struct sock*)ctx->skaddr;
+    // If we're transitioning away from LISTEN but we don't know about this
+    // socket yet then don't report anything.
+    if (ctx->oldstate == BPF_TCP_LISTEN && bpf_map_lookup_elem(&sockmap, &sk) == NULL) {
+        return 0;
+    }
+    // If we aren't transitioning to or from TCP_LISTEN then we don't care.
+    if (ctx->newstate != BPF_TCP_LISTEN && ctx->oldstate != BPF_TCP_LISTEN) {
+        return 0;
+    }
+
+    struct event* ev;
+    ev = bpf_ringbuf_reserve(&events, sizeof(*ev), 0);
+    if (!ev) {
+        return 0;
+    }
+    ev->port = ctx->sport;
+
+    if (ctx->newstate == BPF_TCP_LISTEN) {
+        bpf_map_update_elem(&sockmap, &sk, &set_value, BPF_ANY);
+        ev->state = kPortListenerUp;
+    }
+    if (ctx->oldstate == BPF_TCP_LISTEN) {
+        bpf_map_delete_elem(&sockmap, &sk);
+        ev->state = kPortListenerDown;
+    }
+    bpf_ringbuf_submit(ev, 0);
+
+    return 0;
+}
diff --git a/guest/port_listener/src/main.cc b/guest/port_listener/src/main.cc
new file mode 100644
index 0000000..1caceaf
--- /dev/null
+++ b/guest/port_listener/src/main.cc
@@ -0,0 +1,167 @@
+// Copyright 2024 The Android Open Source Project
+//
+// 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.
+
+// Copied from ChromiumOS with relicensing:
+// src/platform2/vm_tools/port_listener/main.cc
+
+#include <bpf/libbpf.h>
+#include <bpf/libbpf_legacy.h>
+#include <glog/logging.h>
+#include <linux/vm_sockets.h> // Needs to come after sys/socket.h
+#include <sys/socket.h>
+
+#include <memory>
+#include <unordered_map>
+
+#include "common.h"
+#include "listen_tracker.skel.h"
+
+typedef std::unordered_map<int, int> port_usage_map;
+
+namespace port_listener {
+namespace {
+
+int HandleEvent(void* ctx, void* const data, size_t size) {
+    port_usage_map* map = reinterpret_cast<port_usage_map*>(ctx);
+    const struct event* ev = (struct event*)data;
+
+    switch (ev->state) {
+        case kPortListenerUp:
+            (*map)[ev->port]++;
+            break;
+
+        case kPortListenerDown:
+            if ((*map)[ev->port] > 0) {
+                (*map)[ev->port]--;
+            } else {
+                LOG(INFO) << "Received down event while port count was 0; ignoring";
+            }
+
+            break;
+
+        default:
+            LOG(ERROR) << "Unknown event state " << ev->state;
+    }
+
+    LOG(INFO) << "Listen event: port=" << ev->port << " state=" << ev->state;
+
+    return 0;
+}
+
+typedef std::unique_ptr<struct ring_buffer, decltype(&ring_buffer__free)> ring_buffer_ptr;
+typedef std::unique_ptr<listen_tracker_ebpf, decltype(&listen_tracker_ebpf__destroy)>
+        listen_tracker_ptr;
+
+// BPFProgram tracks the state and resources of the listen_tracker BPF program.
+class BPFProgram {
+public:
+    // Default movable but not copyable.
+    BPFProgram(BPFProgram&& other) = default;
+    BPFProgram(const BPFProgram& other) = delete;
+    BPFProgram& operator=(BPFProgram&& other) = default;
+    BPFProgram& operator=(const BPFProgram& other) = delete;
+
+    // Load loads the listen_tracker BPF program and prepares it for polling. On
+    // error nullptr is returned.
+    static std::unique_ptr<BPFProgram> Load() {
+        auto* skel = listen_tracker_ebpf__open();
+        if (!skel) {
+            PLOG(ERROR) << "Failed to open listen_tracker BPF skeleton";
+            return nullptr;
+        }
+        listen_tracker_ptr skeleton(skel, listen_tracker_ebpf__destroy);
+
+        int err = listen_tracker_ebpf__load(skeleton.get());
+        if (err) {
+            PLOG(ERROR) << "Failed to load listen_tracker BPF program";
+            return nullptr;
+        }
+
+        auto map = std::make_unique<port_usage_map>();
+        auto* rb = ring_buffer__new(bpf_map__fd(skel->maps.events), HandleEvent, map.get(), NULL);
+        if (!rb) {
+            PLOG(ERROR) << "Failed to open ring buffer for listen_tracker";
+            return nullptr;
+        }
+        ring_buffer_ptr ringbuf(rb, ring_buffer__free);
+
+        err = listen_tracker_ebpf__attach(skeleton.get());
+        if (err) {
+            PLOG(ERROR) << "Failed to attach listen_tracker";
+            return nullptr;
+        }
+
+        return std::unique_ptr<BPFProgram>(
+                new BPFProgram(std::move(skeleton), std::move(ringbuf), std::move(map)));
+    }
+
+    // Poll waits for the listen_tracker BPF program to post a new event to the
+    // ring buffer. BPFProgram handles integrating this new event into the
+    // port_usage map and callers should consult port_usage() after Poll returns
+    // for the latest data.
+    const bool Poll() {
+        int err = ring_buffer__poll(rb_.get(), -1);
+        if (err < 0) {
+            LOG(ERROR) << "Error polling ring buffer ret=" << err;
+            return false;
+        }
+
+        return true;
+    }
+
+    const port_usage_map& port_usage() { return *port_usage_; }
+
+private:
+    BPFProgram(listen_tracker_ptr&& skeleton, ring_buffer_ptr&& rb,
+               std::unique_ptr<port_usage_map>&& port_usage)
+          : skeleton_(std::move(skeleton)),
+            rb_(std::move(rb)),
+            port_usage_(std::move(port_usage)) {}
+
+    listen_tracker_ptr skeleton_;
+    ring_buffer_ptr rb_;
+    std::unique_ptr<port_usage_map> port_usage_;
+};
+
+} // namespace
+} // namespace port_listener
+
+int main(int argc, char** argv) {
+    google::InitGoogleLogging(argv[0]);
+    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
+
+    // Load our BPF program.
+    auto program = port_listener::BPFProgram::Load();
+    if (program == nullptr) {
+        LOG(ERROR) << "Failed to load BPF program";
+        return EXIT_FAILURE;
+    }
+
+    // main loop: poll for listen updates
+    for (;;) {
+        if (!program->Poll()) {
+            LOG(ERROR) << "Failure while polling BPF program";
+            return EXIT_FAILURE;
+        }
+        // port_usage will be updated with the latest usage data
+
+        for (auto it : program->port_usage()) {
+            if (it.second <= 0) {
+                continue;
+            }
+            // TODO(b/340126051): Add listening TCP4 ports.
+        }
+        // TODO(b/340126051): Notify port information to the guest agent.
+    }
+}
diff --git a/guest/pvmfw/Android.bp b/guest/pvmfw/Android.bp
index abd508b..4586cca 100644
--- a/guest/pvmfw/Android.bp
+++ b/guest/pvmfw/Android.bp
@@ -20,7 +20,6 @@
         "libdiced_open_dice_nostd",
         "liblibfdt_nostd",
         "liblog_rust_nostd",
-        "libonce_cell_nostd",
         "libpvmfw_avb_nostd",
         "libpvmfw_embedded_key",
         "libpvmfw_fdt_template",
diff --git a/guest/pvmfw/README.md b/guest/pvmfw/README.md
index 58ba10c..3ffa3f0 100644
--- a/guest/pvmfw/README.md
+++ b/guest/pvmfw/README.md
@@ -233,10 +233,10 @@
 
 [header]: src/config.rs
 [DTBO]: https://android.googlesource.com/platform/external/dtc/+/refs/heads/main/Documentation/dt-object-internal.txt
-[debug_policy]: ../docs/debug/README.md#debug-policy
-[device_assignment]: ../docs/device_assignment.md
+[debug_policy]: ../../docs/debug/README.md#debug-policy
+[device_assignment]: ../../docs/device_assignment.md
 [secretkeeper_key]: https://android.googlesource.com/platform/system/secretkeeper/+/refs/heads/main/README.md#secretkeeper-public-key
-[vendor_hashtree_digest]: ../microdroid/README.md#verification-of-vendor-image
+[vendor_hashtree_digest]: ../../build/microdroid/README.md#verification-of-vendor-image
 
 #### Virtual Platform DICE Chain Handover
 
@@ -402,7 +402,7 @@
 };
 ```
 
-[dt.md]: ../docs/device_trees.md#avf_specific-properties-and-nodes
+[dt.md]: ../../docs/device_trees.md#avf_specific-properties-and-nodes
 
 ### Guest Image Signing
 
diff --git a/guest/rialto/tests/test.rs b/guest/rialto/tests/test.rs
index 582b69e..09f9cc3 100644
--- a/guest/rialto/tests/test.rs
+++ b/guest/rialto/tests/test.rs
@@ -25,7 +25,10 @@
 use bssl_avf::{rand_bytes, sha256, EcKey, PKey};
 use client_vm_csr::generate_attestation_key_and_csr;
 use coset::{CborSerializable, CoseMac0, CoseSign};
-use hwtrust::{rkp, session::Session};
+use hwtrust::{
+    rkp,
+    session::{RkpInstance, Session},
+};
 use log::{info, warn};
 use service_vm_comm::{
     ClientVmAttestationParams, Csr, CsrPayload, EcdsaP256KeyPair, GenerateCertificateRequestParams,
@@ -206,7 +209,8 @@
     let vm_component = vm_component.decode_as::<asn1::SequenceOf<asn1::Any, 4>>().unwrap();
     assert_eq!(4, vm_component.len());
     let name = vm_component.get(0).unwrap().decode_as::<asn1::Utf8StringRef>().unwrap();
-    assert_eq!(expected_component.name, name.as_ref());
+    let name_str: &str = name.as_ref();
+    assert_eq!(expected_component.name, name_str);
     let version = vm_component.get(1).unwrap().decode_as::<u64>().unwrap();
     assert_eq!(expected_component.version, version);
     let code_hash = vm_component.get(2).unwrap().decode_as::<asn1::OctetString>().unwrap();
@@ -285,6 +289,7 @@
 fn check_csr(csr: Vec<u8>) -> Result<()> {
     let mut session = Session::default();
     session.set_allow_any_mode(true);
+    session.set_rkp_instance(RkpInstance::Avf);
     let _csr = rkp::Csr::from_cbor(&session, &csr[..]).context("Failed to parse CSR")?;
     Ok(())
 }
@@ -335,6 +340,14 @@
     let virtmgr = vmclient::VirtualizationService::new().context("Failed to spawn VirtMgr")?;
     let service = virtmgr.connect().context("Failed to connect to VirtMgr")?;
     info!("Connected to VirtMgr for service VM");
-    VmInstance::create(service.as_ref(), &config, console, /* consoleIn */ None, log, None)
-        .context("Failed to create VM")
+    VmInstance::create(
+        service.as_ref(),
+        &config,
+        console,
+        /* consoleIn */ None,
+        log,
+        /* dump_dt */ None,
+        None,
+    )
+    .context("Failed to create VM")
 }
diff --git a/libs/debian_service/Android.bp b/libs/debian_service/Android.bp
new file mode 100644
index 0000000..0495825
--- /dev/null
+++ b/libs/debian_service/Android.bp
@@ -0,0 +1,56 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+JAVA_LITE_PROTO_CMD = "mkdir -p $(genDir)/gen && " +
+    "$(location aprotoc) --java_opt=annotate_code=false " +
+    "-Iexternal/protobuf/src " +
+    "-Ipackages/modules/Virtualization/libs/debian_service/proto " +
+    "--plugin=protoc-gen-grpc-java=$(location protoc-gen-grpc-java-plugin) " +
+    "--grpc-java_out=lite:$(genDir)/gen $(in) && " +
+    "$(location soong_zip) -o $(out) -C $(genDir)/gen -D $(genDir)/gen"
+
+java_genrule {
+    name: "debian-service-stub-lite",
+    tools: [
+        "aprotoc",
+        "protoc-gen-grpc-java-plugin",
+        "soong_zip",
+    ],
+    cmd: JAVA_LITE_PROTO_CMD,
+    srcs: [
+        "proto/*.proto",
+        ":libprotobuf-internal-protos",
+    ],
+    out: [
+        "protos.srcjar",
+    ],
+}
+
+java_library {
+    name: "debian-service-grpclib-lite",
+    proto: {
+        type: "lite",
+        include_dirs: [
+            "external/protobuf/src",
+            "external/protobuf/java",
+        ],
+    },
+    srcs: [
+        ":debian-service-stub-lite",
+        "proto/*.proto",
+        ":libprotobuf-internal-protos",
+    ],
+    libs: ["javax_annotation-api_1.3.2"],
+    static_libs: [
+        "libprotobuf-java-lite",
+        "grpc-java-core-android",
+        "grpc-java-okhttp-client-lite",
+        "guava",
+    ],
+    sdk_version: "current",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.virt",
+    ],
+}
diff --git a/libs/debian_service/proto/DebianService.proto b/libs/debian_service/proto/DebianService.proto
new file mode 100644
index 0000000..5e3286a
--- /dev/null
+++ b/libs/debian_service/proto/DebianService.proto
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+
+syntax = "proto3";
+
+package com.android.virtualization.vmlauncher.proto;
+
+option java_package = "com.android.virtualization.vmlauncher.proto";
+option java_multiple_files = true;
+
+service DebianService {
+  rpc ReportVmIpAddr (IpAddr) returns (ReportVmIpAddrResponse) {}
+  rpc OpenForwardingRequestQueue (Empty) returns (stream ForwardingRequestItem) {}
+}
+
+message Empty {}
+
+message IpAddr {
+  string addr = 1;
+}
+
+message ReportVmIpAddrResponse {
+  bool success = 1;
+}
+
+message ForwardingRequestItem {
+  int32 guest_tcp_port = 1;
+  int32 vsock_port = 2;
+}
diff --git a/libs/dice/driver/Android.bp b/libs/dice/driver/Android.bp
index c93bd7d..baed21d 100644
--- a/libs/dice/driver/Android.bp
+++ b/libs/dice/driver/Android.bp
@@ -22,7 +22,6 @@
         "liblibc",
         "liblog_rust",
         "libnix",
-        "libonce_cell",
         "libopenssl",
         "libthiserror",
         "libserde_cbor",
diff --git a/libs/framework-virtualization/README.md b/libs/framework-virtualization/README.md
index 0dd7e64..a742ccc 100644
--- a/libs/framework-virtualization/README.md
+++ b/libs/framework-virtualization/README.md
@@ -1,12 +1,12 @@
 # Android Virtualization Framework API
 
 These Java APIs allow an app to configure and run a Virtual Machine running
-[Microdroid](../build/microdroid/README.md) and execute native code from the app (the
+[Microdroid](../../build/microdroid/README.md) and execute native code from the app (the
 payload) within it.
 
-There is more information on AVF [here](../README.md). To see how to package the
+There is more information on AVF [here](../../README.md). To see how to package the
 payload code that is to run inside a VM, and the native API available to it, see
-the [VM Payload API](../libs/libvm_payload/README.md)
+the [VM Payload API](../libvm_payload/README.md)
 
 The API classes are all in the
 [`android.system.virtualmachine`](src/android/system/virtualmachine) package.
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
index 3b16a8a..b278610 100644
--- a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
@@ -1578,7 +1578,8 @@
                                 : createVirtualMachineConfigForAppFrom(vmConfig, service);
 
                 mVirtualMachine =
-                        service.createVm(vmConfigParcel, consoleOutFd, consoleInFd, mLogWriter);
+                        service.createVm(
+                                vmConfigParcel, consoleOutFd, consoleInFd, mLogWriter, null);
                 mVirtualMachine.registerCallback(new CallbackTranslator(service));
                 if (mMemoryManagementCallbacks != null) {
                     mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
diff --git a/libs/libcompos_common/compos_client.rs b/libs/libcompos_common/compos_client.rs
index 107f8d0..316eaa9 100644
--- a/libs/libcompos_common/compos_client.rs
+++ b/libs/libcompos_common/compos_client.rs
@@ -152,6 +152,7 @@
             console_fd,
             /* console_in_fd */ None,
             log_fd,
+            /* dump_dt */ None,
             Some(callback),
         )
         .context("Failed to create VM")?;
diff --git a/libs/libservice_vm_fake_chain/src/service_vm.rs b/libs/libservice_vm_fake_chain/src/service_vm.rs
index 9bd831d..86fd3ea 100644
--- a/libs/libservice_vm_fake_chain/src/service_vm.rs
+++ b/libs/libservice_vm_fake_chain/src/service_vm.rs
@@ -116,6 +116,7 @@
         component_name: Some(cstr!("Protected VM firmware")),
         component_version: Some(1),
         resettable: true,
+        rkp_vm_marker: true,
         ..Default::default()
     };
     let config_descriptor = retry_bcc_format_config_descriptor(&config_values)?;
@@ -158,6 +159,7 @@
         component_name: Some(cstr!("vm_entry")),
         component_version: Some(12),
         resettable: true,
+        rkp_vm_marker: true,
         ..Default::default()
     };
     let config_descriptor = retry_bcc_format_config_descriptor(&config_values)?;
diff --git a/libs/libservice_vm_manager/src/lib.rs b/libs/libservice_vm_manager/src/lib.rs
index d7b4dd6..0f322bb 100644
--- a/libs/libservice_vm_manager/src/lib.rs
+++ b/libs/libservice_vm_manager/src/lib.rs
@@ -244,8 +244,9 @@
     let console_out = Some(android_log_fd()?);
     let console_in = None;
     let log = Some(android_log_fd()?);
+    let dump_dt = None;
     let callback = None;
-    VmInstance::create(service.as_ref(), &config, console_out, console_in, log, callback)
+    VmInstance::create(service.as_ref(), &config, console_out, console_in, log, dump_dt, callback)
         .context("Failed to create service VM")
 }
 
diff --git a/libs/libservice_vm_requests/src/rkp.rs b/libs/libservice_vm_requests/src/rkp.rs
index e2be11b..7de7cd5 100644
--- a/libs/libservice_vm_requests/src/rkp.rs
+++ b/libs/libservice_vm_requests/src/rkp.rs
@@ -63,8 +63,7 @@
 
 const CSR_PAYLOAD_SCHEMA_V3: u8 = 3;
 const AUTH_REQ_SCHEMA_V1: u8 = 1;
-// TODO(b/300624493): Add a new certificate type for AVF CSR.
-const CERTIFICATE_TYPE: &str = "keymint";
+const CERTIFICATE_TYPE: &str = "rkp-vm";
 
 /// Builds the CSR described in:
 ///
diff --git a/libs/libvm_payload/README.md b/libs/libvm_payload/README.md
index 8ef1bac..bbcfc61 100644
--- a/libs/libvm_payload/README.md
+++ b/libs/libvm_payload/README.md
@@ -9,7 +9,7 @@
 available in the VM, and only 64 bit code is supported.
 
 To create a VM and run the payload from Android see the [AVF Java
-APIs](../libs/framework-virtualization/README.md).
+APIs](../framework-virtualization/README.md).
 
 ## Entry point
 
diff --git a/libs/libvmbase/src/entry.rs b/libs/libvmbase/src/entry.rs
index 99f28fc..f442a32 100644
--- a/libs/libvmbase/src/entry.rs
+++ b/libs/libvmbase/src/entry.rs
@@ -56,8 +56,7 @@
 /// This is the entry point to the Rust code, called from the binary entry point in `entry.S`.
 #[no_mangle]
 extern "C" fn rust_entry(x0: u64, x1: u64, x2: u64, x3: u64) -> ! {
-    // SAFETY: Only called once, from here, and inaccessible to client code.
-    unsafe { heap::init() };
+    heap::init();
 
     if try_console_init().is_err() {
         // Don't panic (or log) here to avoid accessing the console.
diff --git a/libs/libvmbase/src/heap.rs b/libs/libvmbase/src/heap.rs
index 99c06aa..3a4e198 100644
--- a/libs/libvmbase/src/heap.rs
+++ b/libs/libvmbase/src/heap.rs
@@ -22,39 +22,78 @@
 use core::ffi::c_void;
 use core::mem;
 use core::num::NonZeroUsize;
+use core::ops::Range;
 use core::ptr;
 use core::ptr::NonNull;
 
 use buddy_system_allocator::LockedHeap;
+use spin::{
+    mutex::{SpinMutex, SpinMutexGuard},
+    Once,
+};
 
 /// Configures the size of the global allocator.
 #[macro_export]
 macro_rules! configure_heap {
     ($len:expr) => {
-        static mut __HEAP_ARRAY: [u8; $len] = [0; $len];
-        #[export_name = "HEAP"]
-        // SAFETY: HEAP will only be accessed once as mut, from init().
-        static mut __HEAP: &'static mut [u8] = unsafe { &mut __HEAP_ARRAY };
+        static __HEAP: $crate::heap::HeapArray<{ $len }> = $crate::heap::HeapArray::new();
+        #[export_name = "get_heap"]
+        fn __get_heap() -> &'static mut [u8] {
+            __HEAP.get()
+        }
     };
 }
 
+/// An array to be used as a heap.
+///
+/// This should be stored in a static variable to have the appropriate lifetime.
+pub struct HeapArray<const SIZE: usize> {
+    array: SpinMutex<[u8; SIZE]>,
+}
+
+impl<const SIZE: usize> HeapArray<SIZE> {
+    /// Creates a new empty heap array.
+    #[allow(clippy::new_without_default)]
+    pub const fn new() -> Self {
+        Self { array: SpinMutex::new([0; SIZE]) }
+    }
+
+    /// Gets the heap as a slice.
+    ///
+    /// Panics if called more than once.
+    pub fn get(&self) -> &mut [u8] {
+        SpinMutexGuard::leak(self.array.try_lock().expect("Page heap was already taken"))
+            .as_mut_slice()
+    }
+}
+
 extern "Rust" {
-    /// Slice used by the global allocator, configured using configure_heap!().
-    static mut HEAP: &'static mut [u8];
+    /// Gets slice used by the global allocator, configured using configure_heap!().
+    ///
+    /// Panics if called more than once.
+    fn get_heap() -> &'static mut [u8];
 }
 
 #[global_allocator]
 static HEAP_ALLOCATOR: LockedHeap<32> = LockedHeap::<32>::new();
 
+/// The range of addresses used for the heap.
+static HEAP_RANGE: Once<Range<usize>> = Once::new();
+
 /// Initialize the global allocator.
 ///
-/// # Safety
-///
-/// Must be called no more than once.
-pub(crate) unsafe fn init() {
-    // SAFETY: Nothing else accesses this memory, and we hand it over to the heap to manage and
-    // never touch it again. The heap is locked, so there cannot be any races.
-    let (start, size) = unsafe { (HEAP.as_mut_ptr() as usize, HEAP.len()) };
+/// Panics if called more than once.
+pub(crate) fn init() {
+    // SAFETY: This is in fact a safe Rust function.
+    let heap = unsafe { get_heap() };
+
+    HEAP_RANGE.call_once(|| {
+        let range = heap.as_ptr_range();
+        range.start as usize..range.end as usize
+    });
+
+    let start = heap.as_mut_ptr() as usize;
+    let size = heap.len();
 
     let mut heap = HEAP_ALLOCATOR.lock();
     // SAFETY: We are supplying a valid memory range, and we only do this once.
@@ -107,10 +146,9 @@
 /// errors.
 unsafe extern "C" fn free(ptr: *mut c_void) {
     let Some(ptr) = NonNull::new(ptr) else { return };
-    // SAFETY: The contents of the HEAP slice may change, but the address range never does.
-    let heap_range = unsafe { HEAP.as_ptr_range() };
+    let heap_range = HEAP_RANGE.get().expect("free called before heap was initialised");
     assert!(
-        heap_range.contains(&(ptr.as_ptr() as *const u8)),
+        heap_range.contains(&(ptr.as_ptr() as usize)),
         "free() called on a pointer that is not part of the HEAP: {ptr:?}"
     );
     // SAFETY: ptr is non-null and was allocated by allocate, which prepends a correctly aligned
diff --git a/libs/libvmclient/src/lib.rs b/libs/libvmclient/src/lib.rs
index ce7d5a5..13630c0 100644
--- a/libs/libvmclient/src/lib.rs
+++ b/libs/libvmclient/src/lib.rs
@@ -208,14 +208,21 @@
         console_out: Option<File>,
         console_in: Option<File>,
         log: Option<File>,
+        dump_dt: Option<File>,
         callback: Option<Box<dyn VmCallback + Send + Sync>>,
     ) -> BinderResult<Self> {
         let console_out = console_out.map(ParcelFileDescriptor::new);
         let console_in = console_in.map(ParcelFileDescriptor::new);
         let log = log.map(ParcelFileDescriptor::new);
+        let dump_dt = dump_dt.map(ParcelFileDescriptor::new);
 
-        let vm =
-            service.createVm(config, console_out.as_ref(), console_in.as_ref(), log.as_ref())?;
+        let vm = service.createVm(
+            config,
+            console_out.as_ref(),
+            console_in.as_ref(),
+            log.as_ref(),
+            dump_dt.as_ref(),
+        )?;
 
         let cid = vm.getCid()?;
 
diff --git a/libs/service-compos/java/com/android/server/compos/IsolatedCompilationService.java b/libs/service-compos/java/com/android/server/compos/IsolatedCompilationService.java
index 95e365d..ab8a4cf 100644
--- a/libs/service-compos/java/com/android/server/compos/IsolatedCompilationService.java
+++ b/libs/service-compos/java/com/android/server/compos/IsolatedCompilationService.java
@@ -104,7 +104,7 @@
                 packageNative.registerStagedApexObserver(observer);
                 // In the unlikely event that an APEX has been staged before we get here, we may
                 // have to schedule compilation immediately.
-                observer.checkModules(packageNative.getStagedApexModuleNames());
+                observer.checkModules(packageNative.getStagedApexInfos());
             } catch (RemoteException e) {
                 Log.e(TAG, "Failed to initialize observer", e);
             }
@@ -118,26 +118,21 @@
         @Override
         public void onApexStaged(ApexStagedEvent event) {
             Log.d(TAG, "onApexStaged");
-            checkModules(event.stagedApexModuleNames);
+            checkModules(event.stagedApexInfos);
         }
 
-        void checkModules(String[] moduleNames) {
+        void checkModules(StagedApexInfo[] stagedApexInfos) {
             if (IsolatedCompilationJobService.isStagedApexJobScheduled(mScheduler)) {
                 Log.d(TAG, "Job already scheduled");
                 // We're going to run anyway, we don't need to check this update
                 return;
             }
             boolean needCompilation = false;
-            for (String moduleName : moduleNames) {
-                try {
-                    StagedApexInfo apexInfo = mPackageNative.getStagedApexInfo(moduleName);
-                    if (apexInfo != null && apexInfo.hasClassPathJars) {
-                        Log.i(TAG, "Classpath affecting module updated: " + moduleName);
-                        needCompilation = true;
-                        break;
-                    }
-                } catch (RemoteException e) {
-                    Log.w(TAG, "Failed to get getStagedApexInfo for " + moduleName);
+            for (StagedApexInfo apexInfo : stagedApexInfos) {
+                if (apexInfo != null && apexInfo.hasClassPathJars) {
+                    Log.i(TAG, "Classpath affecting module updated: " + apexInfo.moduleName);
+                    needCompilation = true;
+                    break;
                 }
             }
             if (needCompilation) {
diff --git a/libs/service-virtualization/src/com/android/system/virtualmachine/VirtualizationSystemService.java b/libs/service-virtualization/src/com/android/system/virtualmachine/VirtualizationSystemService.java
index 241eef4..998389b 100644
--- a/libs/service-virtualization/src/com/android/system/virtualmachine/VirtualizationSystemService.java
+++ b/libs/service-virtualization/src/com/android/system/virtualmachine/VirtualizationSystemService.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.net.LinkAddress;
 import android.net.TetheringManager;
 import android.net.TetheringManager.StartTetheringCallback;
 import android.net.TetheringManager.TetheringRequest;
@@ -157,8 +158,11 @@
 
         @Override
         public void enableVmTethering() {
+            LinkAddress local = new LinkAddress("192.168.0.1/24");
+            LinkAddress client = new LinkAddress("192.168.0.2/24");
             final TetheringRequest tr =
                     new TetheringRequest.Builder(TetheringManager.TETHERING_VIRTUAL)
+                            .setStaticIpv4Addresses(local, client)
                             .setConnectivityScope(TetheringManager.CONNECTIVITY_SCOPE_GLOBAL)
                             .build();
 
diff --git a/libs/vm_launcher_lib/Android.bp b/libs/vm_launcher_lib/Android.bp
index cb6fc9e..f47f6b6 100644
--- a/libs/vm_launcher_lib/Android.bp
+++ b/libs/vm_launcher_lib/Android.bp
@@ -12,6 +12,8 @@
     platform_apis: true,
     static_libs: [
         "gson",
+        "debian-service-grpclib-lite",
+        "apache-commons-compress",
     ],
     libs: [
         "framework-virtualization.impl",
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/DebianServiceImpl.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/DebianServiceImpl.java
new file mode 100644
index 0000000..ccc0ed6
--- /dev/null
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/DebianServiceImpl.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.virtualization.vmlauncher;
+
+import android.util.Log;
+
+import com.android.virtualization.vmlauncher.proto.DebianServiceGrpc;
+import com.android.virtualization.vmlauncher.proto.Empty;
+import com.android.virtualization.vmlauncher.proto.ForwardingRequestItem;
+import com.android.virtualization.vmlauncher.proto.IpAddr;
+import com.android.virtualization.vmlauncher.proto.ReportVmIpAddrResponse;
+
+import io.grpc.stub.StreamObserver;
+
+class DebianServiceImpl extends DebianServiceGrpc.DebianServiceImplBase {
+    public static final String TAG = "DebianService";
+    private final DebianServiceCallback mCallback;
+
+    protected DebianServiceImpl(DebianServiceCallback callback) {
+        super();
+        mCallback = callback;
+    }
+
+    @Override
+    public void reportVmIpAddr(
+            IpAddr request, StreamObserver<ReportVmIpAddrResponse> responseObserver) {
+        Log.d(DebianServiceImpl.TAG, "reportVmIpAddr: " + request.toString());
+        mCallback.onIpAddressAvailable(request.getAddr());
+        ReportVmIpAddrResponse reply = ReportVmIpAddrResponse.newBuilder().setSuccess(true).build();
+        responseObserver.onNext(reply);
+        responseObserver.onCompleted();
+    }
+
+    @Override
+    public void openForwardingRequestQueue(
+            Empty request, StreamObserver<ForwardingRequestItem> responseObserver) {
+        Log.d(DebianServiceImpl.TAG, "OpenForwardingRequestQueue");
+
+        // TODO(b/340126051): Bring information from forwarder_host.
+
+        responseObserver.onCompleted();
+    }
+
+    protected interface DebianServiceCallback {
+        void onIpAddressAvailable(String ipAddr);
+    }
+}
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
new file mode 100644
index 0000000..eb6dd77
--- /dev/null
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+package com.android.virtualization.vmlauncher;
+
+import android.content.Context;
+import android.os.Environment;
+import android.util.Log;
+
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+public class InstallUtils {
+    private static final String TAG = InstallUtils.class.getSimpleName();
+
+    private static final String VM_CONFIG_FILENAME = "vm_config.json";
+    private static final String COMPRESSED_PAYLOAD_FILENAME = "images.tar.gz";
+    private static final String PAYLOAD_DIR = "linux";
+
+    public static String getVmConfigPath(Context context) {
+        return new File(context.getFilesDir(), PAYLOAD_DIR)
+                .toPath()
+                .resolve(VM_CONFIG_FILENAME)
+                .toString();
+    }
+
+    public static boolean isImageInstalled(Context context) {
+        return Files.exists(Path.of(getVmConfigPath(context)));
+    }
+
+    private static Path getPayloadPath() {
+        File payloadDir = Environment.getExternalStoragePublicDirectory(PAYLOAD_DIR);
+        if (payloadDir == null) {
+            Log.d(TAG, "no payload dir: " + payloadDir);
+            return null;
+        }
+        Path payloadPath = payloadDir.toPath().resolve(COMPRESSED_PAYLOAD_FILENAME);
+        return payloadPath;
+    }
+
+    public static boolean payloadFromExternalStorageExists() {
+        return Files.exists(getPayloadPath());
+    }
+
+    public static boolean installImageFromExternalStorage(Context context) {
+        if (!payloadFromExternalStorageExists()) {
+            Log.d(TAG, "no artifact file from external storage");
+        }
+        Path payloadPath = getPayloadPath();
+        try (BufferedInputStream inputStream =
+                        new BufferedInputStream(Files.newInputStream(payloadPath));
+                TarArchiveInputStream tar =
+                        new TarArchiveInputStream(new GzipCompressorInputStream(inputStream))) {
+            ArchiveEntry entry;
+            Path baseDir = new File(context.getFilesDir(), PAYLOAD_DIR).toPath();
+            Files.createDirectories(baseDir);
+            while ((entry = tar.getNextEntry()) != null) {
+                Path extractTo = baseDir.resolve(entry.getName());
+                if (entry.isDirectory()) {
+                    Files.createDirectories(extractTo);
+                } else {
+                    Files.copy(tar, extractTo, StandardCopyOption.REPLACE_EXISTING);
+                }
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "installation failed", e);
+            return false;
+        }
+        if (!isImageInstalled(context)) {
+            return false;
+        }
+
+        if (!resolvePathInVmConfig(context)) {
+            Log.d(TAG, "resolving path failed");
+            try {
+                Files.deleteIfExists(Path.of(getVmConfigPath(context)));
+            } catch (IOException e) {
+                return false;
+            }
+            return false;
+        }
+
+        // remove payload if installation is done.
+        try {
+            Files.deleteIfExists(payloadPath);
+        } catch (IOException e) {
+            Log.d(TAG, "failed to remove installed payload", e);
+        }
+
+        return true;
+    }
+
+    private static Function<String, String> getReplacer(Context context) {
+        Map<String, String> rules = new HashMap<>();
+        rules.put("\\$PAYLOAD_DIR", new File(context.getFilesDir(), PAYLOAD_DIR).toString());
+        return (s) -> {
+            for (Map.Entry<String, String> rule : rules.entrySet()) {
+                s = s.replaceAll(rule.getKey(), rule.getValue());
+            }
+            return s;
+        };
+    }
+
+    private static boolean resolvePathInVmConfig(Context context) {
+        try {
+            String replacedVmConfig =
+                    String.join(
+                            "\n",
+                            Files.readAllLines(Path.of(getVmConfigPath(context))).stream()
+                                    .map(getReplacer(context))
+                                    .toList());
+            Files.write(Path.of(getVmConfigPath(context)), replacedVmConfig.getBytes());
+            return true;
+        } catch (IOException e) {
+            return false;
+        }
+    }
+}
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
index 5e78f99..849cc24 100644
--- a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
@@ -17,33 +17,28 @@
 package com.android.virtualization.vmlauncher;
 
 import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
 import android.app.Service;
 import android.content.Intent;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.IBinder;
-import android.os.Looper;
-import android.os.ParcelFileDescriptor;
 import android.os.ResultReceiver;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineConfig;
 import android.system.virtualmachine.VirtualMachineException;
 import android.util.Log;
 
-import java.io.BufferedReader;
-import java.io.FileInputStream;
+import io.grpc.InsecureServerCredentials;
+import io.grpc.Server;
+import io.grpc.okhttp.OkHttpServerBuilder;
+
 import java.io.IOException;
-import java.io.InputStreamReader;
 import java.nio.file.Path;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
-public class VmLauncherService extends Service {
+public class VmLauncherService extends Service implements DebianServiceImpl.DebianServiceCallback {
+    public static final String EXTRA_NOTIFICATION = "EXTRA_NOTIFICATION";
     private static final String TAG = "VmLauncherService";
-    // TODO: this path should be from outside of this service
-    private static final String VM_CONFIG_PATH = "/data/local/tmp/vm_config.json";
 
     private static final int RESULT_START = 0;
     private static final int RESULT_STOP = 1;
@@ -54,24 +49,15 @@
     private ExecutorService mExecutorService;
     private VirtualMachine mVirtualMachine;
     private ResultReceiver mResultReceiver;
+    private Server mServer;
 
     @Override
     public IBinder onBind(Intent intent) {
         return null;
     }
 
-    private void startForeground() {
-        NotificationManager notificationManager = getSystemService(NotificationManager.class);
-        NotificationChannel notificationChannel =
-                new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_LOW);
-        notificationManager.createNotificationChannel(notificationChannel);
-        startForeground(
-                this.hashCode(),
-                new Notification.Builder(this, TAG)
-                        .setChannelId(TAG)
-                        .setSmallIcon(android.R.drawable.ic_dialog_info)
-                        .setContentText("A VM " + mVirtualMachine.getName() + " is running")
-                        .build());
+    private void startForeground(Notification notification) {
+        startForeground(this.hashCode(), notification);
     }
 
     @Override
@@ -82,12 +68,15 @@
         }
         mExecutorService = Executors.newCachedThreadPool();
 
-        ConfigJson json = ConfigJson.from(VM_CONFIG_PATH);
+        ConfigJson json = ConfigJson.from(InstallUtils.getVmConfigPath(this));
         VirtualMachineConfig config = json.toConfig(this);
 
         Runner runner;
         try {
+            android.os.Trace.beginSection("vmCreate");
             runner = Runner.create(this, config);
+            android.os.Trace.endSection();
+            android.os.Trace.beginAsyncSection("debianBoot", 0);
         } catch (VirtualMachineException e) {
             Log.e(TAG, "cannot create runner", e);
             stopSelf();
@@ -110,13 +99,15 @@
         Path logPath = getFileStreamPath(mVirtualMachine.getName() + ".log").toPath();
         Logger.setup(mVirtualMachine, logPath, mExecutorService);
 
-        startForeground();
+        Notification notification = intent.getParcelableExtra(EXTRA_NOTIFICATION,
+                Notification.class);
+
+        startForeground(notification);
 
         mResultReceiver.send(RESULT_START, null);
-        if (config.getCustomImageConfig().useNetwork()) {
-            Handler handler = new Handler(Looper.getMainLooper());
-            gatherIpAddrFromVm(handler);
-        }
+
+        startDebianServer();
+
         return START_NOT_STICKY;
     }
 
@@ -134,6 +125,7 @@
             mExecutorService = null;
             mVirtualMachine = null;
         }
+        stopDebianServer();
     }
 
     private boolean isVmRunning() {
@@ -141,34 +133,38 @@
                 && mVirtualMachine.getStatus() == VirtualMachine.STATUS_RUNNING;
     }
 
-    // TODO(b/359523803): Use AVF API to get ip addr when it exists
-    private void gatherIpAddrFromVm(Handler handler) {
-        handler.postDelayed(
-                () -> {
-                    if (!isVmRunning()) {
-                        Log.d(TAG, "A virtual machine instance isn't running");
-                        return;
-                    }
-                    int INTERNAL_VSOCK_SERVER_PORT = 1024;
-                    try (ParcelFileDescriptor pfd =
-                            mVirtualMachine.connectVsock(INTERNAL_VSOCK_SERVER_PORT)) {
-                        try (BufferedReader input =
-                                new BufferedReader(
-                                        new InputStreamReader(
-                                                new FileInputStream(pfd.getFileDescriptor())))) {
-                            String vmIpAddr = input.readLine().strip();
-                            Bundle b = new Bundle();
-                            b.putString(KEY_VM_IP_ADDR, vmIpAddr);
-                            mResultReceiver.send(RESULT_IPADDR, b);
-                            return;
-                        } catch (IOException e) {
-                            Log.e(TAG, e.toString());
-                        }
-                    } catch (Exception e) {
-                        Log.e(TAG, e.toString());
-                    }
-                    gatherIpAddrFromVm(handler);
-                },
-                1000);
+    private void startDebianServer() {
+        new Thread(
+                        () -> {
+                            // TODO(b/372666638): gRPC for java doesn't support vsock for now.
+                            // In addition, let's consider using a dynamic port and SSL(and client
+                            // certificate)
+                            int port = 12000;
+                            try {
+                                mServer =
+                                        OkHttpServerBuilder.forPort(
+                                                        port, InsecureServerCredentials.create())
+                                                .addService(new DebianServiceImpl(this))
+                                                .build()
+                                                .start();
+                            } catch (IOException e) {
+                                Log.d(TAG, "grpc server error", e);
+                            }
+                        })
+                .start();
+    }
+
+    private void stopDebianServer() {
+        if (mServer != null) {
+            mServer.shutdown();
+        }
+    }
+
+    @Override
+    public void onIpAddressAvailable(String ipAddr) {
+        android.os.Trace.endAsyncSection("debianBoot", 0);
+        Bundle b = new Bundle();
+        b.putString(VmLauncherService.KEY_VM_IP_ADDR, ipAddr);
+        mResultReceiver.send(VmLauncherService.RESULT_IPADDR, b);
     }
 }
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
index 565b793..2fa0b32 100644
--- a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
@@ -16,6 +16,7 @@
 
 package com.android.virtualization.vmlauncher;
 
+import android.app.Notification;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -64,7 +65,8 @@
         context.stopService(i);
     }
 
-    public static void startVmLauncherService(Context context, VmLauncherServiceCallback callback) {
+    public static void startVmLauncherService(Context context, VmLauncherServiceCallback callback,
+            Notification notification) {
         Intent i = buildVmLauncherServiceIntent(context);
         if (i == null) {
             return;
@@ -93,6 +95,7 @@
                     }
                 };
         i.putExtra(Intent.EXTRA_RESULT_RECEIVER, getResultReceiverForIntent(resultReceiver));
+        i.putExtra(VmLauncherService.EXTRA_NOTIFICATION, notification);
         context.startForegroundService(i);
     }
 
diff --git a/microfuchsia/microfuchsiad/src/instance_starter.rs b/microfuchsia/microfuchsiad/src/instance_starter.rs
index 15fcc06..61a024f 100644
--- a/microfuchsia/microfuchsiad/src/instance_starter.rs
+++ b/microfuchsia/microfuchsiad/src/instance_starter.rs
@@ -90,6 +90,7 @@
             console_out,
             console_in,
             /* log= */ None,
+            /* dump_dt= */ None,
             None,
         )
         .context("Failed to create VM")?;
diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp
index d0838a6..1f86d6b 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -26,6 +26,7 @@
         ":microdroid_general_sepolicy.conf",
         ":test.com.android.virt.pem",
         ":test2.com.android.virt.pem",
+        "java/**/goldens/dt_dump_*",
     ],
     data_native_bins: [
         "sepolicy-analyze",
@@ -38,6 +39,7 @@
         "lz4",
         "sign_virt_apex",
         "simg2img",
+        "dtc",
     ],
     // java_test_host doesn't have data_native_libs but jni_libs can be used to put
     // native modules under ./lib directory.
@@ -48,6 +50,7 @@
         "libcrypto_utils",
         "libcrypto",
         "libext4_utils",
+        "libfdt",
         "liblog",
         "liblp",
         "libsparse",
diff --git a/tests/hostside/AndroidTest.xml b/tests/hostside/AndroidTest.xml
index 18728ad..f77def3 100644
--- a/tests/hostside/AndroidTest.xml
+++ b/tests/hostside/AndroidTest.xml
@@ -28,4 +28,8 @@
     <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
         <option name="jar" value="MicrodroidHostTestCases.jar" />
     </test>
+
+    <!-- Controller that will skip the module if a native bridge situation is detected -->
+    <!-- For example: module wants to run arm and device is x86 -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.NativeBridgeModuleController" />
 </configuration>
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 2d55d66..83adc91 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -36,6 +36,7 @@
 import android.cts.statsdatom.lib.ReportUtils;
 
 import com.android.compatibility.common.util.CddTest;
+import com.android.compatibility.common.util.PropertyUtil;
 import com.android.compatibility.common.util.VsrTest;
 import com.android.microdroid.test.common.ProcessUtil;
 import com.android.microdroid.test.host.CommandRunner;
@@ -437,9 +438,8 @@
     @VsrTest(requirements = {"VSR-7.1-001.008"})
     public void UpgradedPackageIsAcceptedWithSecretkeeper() throws Exception {
         // Preconditions
-        assumeVmTypeSupported(true);
-        assumeUpdatableVmSupported();
-
+        assumeVmTypeSupported(true); // Non-protected VMs may not support upgrades
+        ensureUpdatableVmSupported();
         getDevice().uninstallPackage(PACKAGE_NAME);
         getDevice().installPackage(findTestFile(APK_NAME), /* reinstall= */ true);
         ensureProtectedMicrodroidBootsSuccessfully(INSTANCE_ID_FILE, INSTANCE_IMG);
@@ -1352,6 +1352,136 @@
         }
     }
 
+    @Test
+    @Parameters(method = "gkiVersions")
+    @TestCaseName("{method}_gki_{0}")
+    public void microdroidDeviceTreeCompat(String gki) throws Exception {
+        assumeArm64Supported();
+        final String configPath = "assets/vm_config.json";
+        // Preconditions
+        assumeKernelSupported(gki);
+        int mem_size = 256;
+        assertTrue("Memory size too small", mem_size >= minMemorySize());
+
+        // Start the VM with the dump DT option.
+        mMicrodroidDevice =
+                MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
+                        .debugLevel("full")
+                        .memoryMib(mem_size)
+                        .cpuTopology("one_cpu")
+                        .protectedVm(false)
+                        .gki(sGkiVersions.get(gki))
+                        .name("test_device_tree")
+                        .dumpDt("/data/local/tmp/dump_dt.dtb")
+                        .build(getAndroidDevice());
+        assertThat(mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT)).isTrue();
+
+        File goldenDt = findTestFile("dt_dump_golden.dts");
+        testGoldenDeviceTree(goldenDt.getAbsolutePath());
+    }
+
+    @Test
+    @Parameters(method = "gkiVersions")
+    @TestCaseName("{method}_gki_{0}")
+    public void microdroidProtectedDeviceTreeCompat(String gki) throws Exception {
+        assumeArm64Supported();
+        final String configPath = "assets/vm_config.json";
+        // Preconditions
+        assumeKernelSupported(gki);
+        assumeVmTypeSupported(true);
+        int mem_size = 256;
+        assertTrue("Memory size too small", mem_size >= minMemorySize());
+
+        // Start the VM with the dump DT option.
+        mMicrodroidDevice =
+                MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
+                        .debugLevel("full")
+                        .memoryMib(mem_size)
+                        .cpuTopology("one_cpu")
+                        .protectedVm(true)
+                        .gki(sGkiVersions.get(gki))
+                        .name("test_device_tree")
+                        .dumpDt("/data/local/tmp/dump_dt.dtb")
+                        .build(getAndroidDevice());
+        assertThat(mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT)).isTrue();
+
+        File goldenDt = findTestFile("dt_dump_protected_golden.dts");
+        testGoldenDeviceTree(goldenDt.getAbsolutePath());
+    }
+
+    private void testGoldenDeviceTree(String goldenDt) throws Exception {
+        // Pull the device tree to host.
+        TestDevice device = getAndroidDevice();
+        boolean disableRoot = !device.isAdbRoot();
+        device.enableAdbRoot();
+        assumeTrue("adb root is not enabled", device.isAdbRoot());
+
+        // Pull DT from device
+        File dtb_from_device = device.pullFile("/data/local/tmp/dump_dt.dtb");
+        if (disableRoot) {
+            device.disableAdbRoot();
+        }
+
+        File dtc = findTestFile("dtc");
+
+        // Create temp file for Device tree conversion
+        File dt_dump_dts = File.createTempFile("dt_dump", "dts");
+        dt_dump_dts.delete();
+        String dt_dump_dts_path = dt_dump_dts.getAbsolutePath();
+        // Convert DT to text format.
+        CommandResult dtb_to_dts =
+                RunUtil.getDefault()
+                        .runTimedCmd(
+                                3000,
+                                dtc.getAbsolutePath(),
+                                "-I",
+                                "dtb",
+                                "-O",
+                                "dts",
+                                "-qqq",
+                                "-f",
+                                "-s",
+                                "-o",
+                                dt_dump_dts_path,
+                                dtb_from_device.getAbsolutePath());
+        assertTrue(
+                "result convert stderr: " + dtb_to_dts.getStderr(),
+                dtb_to_dts.getStderr().trim().isEmpty());
+        assertTrue(
+                "result convert stdout: " + dtb_to_dts.getStdout(),
+                dtb_to_dts.getStdout().trim().isEmpty());
+
+        // Diff device's DT with the golden DT.
+        CommandResult result_compare =
+                RunUtil.getDefault()
+                        .runTimedCmd(
+                                3000,
+                                "diff",
+                                "-u",
+                                "-w",
+                                "-I",
+                                "kaslr-seed",
+                                "-I",
+                                "instance-id",
+                                "-I",
+                                "rng-seed",
+                                "-I",
+                                "linux,initrd-end",
+                                "-I",
+                                "secretkeeper_public_key",
+                                "-I",
+                                "interrupt-map",
+                                dt_dump_dts_path,
+                                goldenDt);
+
+        assertTrue(
+                "result compare stderr: " + result_compare.getStderr(),
+                result_compare.getStderr().trim().isEmpty());
+        assertTrue(
+                "result compare stdout: " + result_compare.getStdout(),
+                result_compare.getStdout().trim().isEmpty());
+    }
+
     @Before
     public void setUp() throws Exception {
         assumeDeviceIsCapable(getDevice());
@@ -1392,10 +1522,16 @@
                         && device.doesFileExist("/sys/bus/platform/drivers/vfio-platform"));
     }
 
-    private void assumeUpdatableVmSupported() throws DeviceNotAvailableException {
-        assumeTrue(
-                "This test is only applicable if if Updatable VMs are supported",
-                isUpdatableVmSupported());
+    private void ensureUpdatableVmSupported() throws DeviceNotAvailableException {
+        if (PropertyUtil.isVendorApiLevelAtLeast(getAndroidDevice(), 202504)) {
+            assertTrue(
+                    "Missing Updatable VM support, have you declared Secretkeeper interface?",
+                    isUpdatableVmSupported());
+        } else {
+            assumeTrue(
+                    "Vendor API lower than 202504 may not support Updatable VM",
+                    isUpdatableVmSupported());
+        }
     }
 
     private TestDevice getAndroidDevice() {
@@ -1428,4 +1564,11 @@
                 "Microdroid is not supported for specific VM protection type",
                 getAndroidDevice().supportsMicrodroid(protectedVm));
     }
+
+    private void assumeArm64Supported() throws Exception {
+        CommandRunner android = new CommandRunner(getDevice());
+        String abi = android.run("getprop", "ro.product.cpu.abi");
+        assertThat(abi).isNotEmpty();
+        assumeTrue("Skipping test as the architecture is not supported", abi.startsWith("arm64"));
+    }
 }
diff --git a/tests/hostside/java/com/android/microdroid/test/goldens/dt_dump_golden.dts b/tests/hostside/java/com/android/microdroid/test/goldens/dt_dump_golden.dts
new file mode 100644
index 0000000..795c50f
--- /dev/null
+++ b/tests/hostside/java/com/android/microdroid/test/goldens/dt_dump_golden.dts
@@ -0,0 +1,145 @@
+/dts-v1/;
+
+/ {
+        #address-cells = <0x02>;
+        #size-cells = <0x02>;
+        compatible = "linux,dummy-virt";
+        interrupt-parent = <0x01>;
+        name = "reference";
+
+        U6_16550A@2e8 {
+                clock-frequency = <0x1c2000>;
+                compatible = "ns16550a";
+                interrupts = <0x00 0x02 0x01>;
+                reg = <0x00 0x2e8 0x00 0x08>;
+        };
+
+        U6_16550A@2f8 {
+                clock-frequency = <0x1c2000>;
+                compatible = "ns16550a";
+                interrupts = <0x00 0x02 0x01>;
+                reg = <0x00 0x2f8 0x00 0x08>;
+        };
+
+        U6_16550A@3e8 {
+                clock-frequency = <0x1c2000>;
+                compatible = "ns16550a";
+                interrupts = <0x00 0x00 0x01>;
+                reg = <0x00 0x3e8 0x00 0x08>;
+        };
+
+        U6_16550A@3f8 {
+                clock-frequency = <0x1c2000>;
+                compatible = "ns16550a";
+                interrupts = <0x00 0x00 0x01>;
+                reg = <0x00 0x3f8 0x00 0x08>;
+        };
+
+        __symbols__ {
+                intc = "/intc";
+        };
+
+        avf {
+                secretkeeper_public_key = [];
+
+                untrusted {
+                        defer-rollback-protection;
+                        instance-id = <0xf145d4f8 0x15f03952 0x5af249aa 0xfead94d8 0xb9f05746 0xd9163f48 0x7251b67b 0xe117409e 0x2b14dfa5 0xcaa8caf7 0x14176d2d 0xf88cc94b 0xeed4a59d 0x9a2d8fe5 0x5ac590f1 0xbb6c96f5>;
+                };
+        };
+
+        chosen {
+                bootargs = "panic=-1 crashkernel=17M";
+                kaslr-seed = <>;
+                linux,initrd-end = <0x81200360>;
+                linux,initrd-start = <0x81000000>;
+                linux,pci-probe-only = <0x01>;
+                rng-seed = <>;
+                stdout-path = "/U6_16550A@3f8";
+        };
+
+        config {
+                kernel-address = <0x80000000>;
+                kernel-size = <0xc91000>;
+        };
+
+        cpufreq {
+                compatible = "virtual,kvm-cpufreq";
+        };
+
+        cpus {
+                #address-cells = <0x01>;
+                #size-cells = <0x00>;
+
+                cpu@0 {
+                        compatible = "arm,armv8";
+                        device_type = "cpu";
+                        phandle = <0x100>;
+                        reg = <0x00>;
+                };
+        };
+
+        intc {
+                #address-cells = <0x02>;
+                #interrupt-cells = <0x03>;
+                #size-cells = <0x02>;
+                compatible = "arm,gic-v3";
+                interrupt-controller;
+                phandle = <0x01>;
+                reg = <0x00 0x3fff0000 0x00 0x10000 0x00 0x3ffd0000 0x00 0x20000>;
+        };
+
+        memory {
+                device_type = "memory";
+                reg = <0x00 0x80000000 0x00 0x10000000>;
+        };
+
+        pci {
+                #address-cells = <0x03>;
+                #interrupt-cells = <0x01>;
+                #size-cells = <0x02>;
+                bus-range = <0x00 0x00>;
+                compatible = "pci-host-cam-generic";
+                device_type = "pci";
+                dma-coherent;
+                interrupt-map = <0x800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x04 0x04 0x1000 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x05 0x04 0x1800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x06 0x04 0x2000 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x07 0x04 0x2800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x08 0x04 0x3000 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x09 0x04 0x3800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x0a 0x04 0x4000 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x0b 0x04 0x4800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x0c 0x04>;
+                interrupt-map-mask = <0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07>;
+                ranges = <0x3000000 0x00 0x2000000 0x00 0x2000000 0x00 0x2000000 0x43000000 0x00 0x90800000 0x00 0x90800000 0xff 0x6f800000>;
+                reg = <0x00 0x10000 0x00 0x1000000>;
+        };
+
+        pclk@3M {
+                #clock-cells = <0x00>;
+                clock-frequency = <0x2fefd8>;
+                compatible = "fixed-clock";
+                phandle = <0x18>;
+        };
+
+        psci {
+                compatible = "arm,psci-1.0\0arm,psci-0.2";
+                method = "hvc";
+        };
+
+        rtc@2000 {
+                arm,primecell-periphid = <0x41030>;
+                clock-names = "apb_pclk";
+                clocks = <0x18>;
+                compatible = "arm,primecell";
+                interrupts = <0x00 0x01 0x04>;
+                reg = <0x00 0x2000 0x00 0x1000>;
+        };
+
+        timer {
+                always-on;
+                compatible = "arm,armv8-timer";
+                interrupts = <0x01 0x0d 0x108 0x01 0x0e 0x108 0x01 0x0b 0x108 0x01 0x0a 0x108>;
+        };
+
+        vmwdt@3000 {
+                clock-frequency = <0x02>;
+                compatible = "qemu,vcpu-stall-detector";
+                interrupts = <0x01 0x0f 0x101>;
+                reg = <0x00 0x3000 0x00 0x1000>;
+                timeout-sec = <0x0a>;
+        };
+};
diff --git a/tests/hostside/java/com/android/microdroid/test/goldens/dt_dump_protected_golden.dts b/tests/hostside/java/com/android/microdroid/test/goldens/dt_dump_protected_golden.dts
new file mode 100644
index 0000000..5761c15
--- /dev/null
+++ b/tests/hostside/java/com/android/microdroid/test/goldens/dt_dump_protected_golden.dts
@@ -0,0 +1,159 @@
+/dts-v1/;
+
+/ {
+        #address-cells = <0x02>;
+        #size-cells = <0x02>;
+        compatible = "linux,dummy-virt";
+        interrupt-parent = <0x01>;
+        name = "reference";
+
+        U6_16550A@2e8 {
+                clock-frequency = <0x1c2000>;
+                compatible = "ns16550a";
+                interrupts = <0x00 0x02 0x01>;
+                reg = <0x00 0x2e8 0x00 0x08>;
+        };
+
+        U6_16550A@2f8 {
+                clock-frequency = <0x1c2000>;
+                compatible = "ns16550a";
+                interrupts = <0x00 0x02 0x01>;
+                reg = <0x00 0x2f8 0x00 0x08>;
+        };
+
+        U6_16550A@3e8 {
+                clock-frequency = <0x1c2000>;
+                compatible = "ns16550a";
+                interrupts = <0x00 0x00 0x01>;
+                reg = <0x00 0x3e8 0x00 0x08>;
+        };
+
+        U6_16550A@3f8 {
+                clock-frequency = <0x1c2000>;
+                compatible = "ns16550a";
+                interrupts = <0x00 0x00 0x01>;
+                reg = <0x00 0x3f8 0x00 0x08>;
+        };
+
+        __symbols__ {
+                intc = "/intc";
+        };
+
+        avf {
+                secretkeeper_public_key = [];
+
+                untrusted {
+                        defer-rollback-protection;
+                        instance-id = <0x4d482941 0x27228238 0x11d7b28 0xaeed3076 0x88eb3fcb 0x2b9de301 0x57ff8977 0xaf8c24b6 0x55466af4 0x23beed37 0x2f976083 0xe630eb28 0x1edbc491 0xa8300897 0xeb3e9f76 0x21ea9284>;
+                };
+        };
+
+        chosen {
+                bootargs = "panic=-1 crashkernel=31M";
+                kaslr-seed = <>;
+                linux,initrd-end = <0x81202104>;
+                linux,initrd-start = <0x81000000>;
+                linux,pci-probe-only = <0x01>;
+                rng-seed = <>;
+                stdout-path = "/U6_16550A@3f8";
+        };
+
+        config {
+                kernel-address = <0x80000000>;
+                kernel-size = <0xc91000>;
+        };
+
+        cpufreq {
+                compatible = "virtual,kvm-cpufreq";
+        };
+
+        cpus {
+                #address-cells = <0x01>;
+                #size-cells = <0x00>;
+
+                cpu@0 {
+                        compatible = "arm,armv8";
+                        device_type = "cpu";
+                        phandle = <0x100>;
+                        reg = <0x00>;
+                };
+        };
+
+        intc {
+                #address-cells = <0x02>;
+                #interrupt-cells = <0x03>;
+                #size-cells = <0x02>;
+                compatible = "arm,gic-v3";
+                interrupt-controller;
+                phandle = <0x01>;
+                reg = <0x00 0x3fff0000 0x00 0x10000 0x00 0x3ffd0000 0x00 0x20000>;
+        };
+
+        memory {
+                device_type = "memory";
+                reg = <0x00 0x80000000 0x00 0x10e00000>;
+        };
+
+        pci {
+                #address-cells = <0x03>;
+                #interrupt-cells = <0x01>;
+                #size-cells = <0x02>;
+                bus-range = <0x00 0x00>;
+                compatible = "pci-host-cam-generic";
+                device_type = "pci";
+                dma-coherent;
+                interrupt-map = <0x800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x04 0x04 0x1000 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x05 0x04 0x1800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x06 0x04 0x2000 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x07 0x04 0x2800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x08 0x04 0x3000 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x09 0x04 0x3800 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x0a 0x04 0x4000 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x0b 0x04>;
+                interrupt-map-mask = <0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07 0xf800 0x00 0x00 0x07>;
+                memory-region = <0x02>;
+                ranges = <0x3000000 0x00 0x2000000 0x00 0x2000000 0x00 0x2000000 0x43000000 0x00 0x91600000 0x00 0x91600000 0xff 0x6ea00000>;
+                reg = <0x00 0x10000 0x00 0x1000000>;
+        };
+
+        pclk@3M {
+                #clock-cells = <0x00>;
+                clock-frequency = <0x2fefd8>;
+                compatible = "fixed-clock";
+                phandle = <0x18>;
+        };
+
+        psci {
+                compatible = "arm,psci-1.0\0arm,psci-0.2";
+                method = "hvc";
+        };
+
+        reserved-memory {
+                #address-cells = <0x02>;
+                #size-cells = <0x02>;
+                ranges;
+
+                restricted_dma_reserved {
+                        alignment = <0x00 0x1000>;
+                        compatible = "restricted-dma-pool";
+                        phandle = <0x02>;
+                        size = <0x00 0xe00000>;
+                };
+        };
+
+        rtc@2000 {
+                arm,primecell-periphid = <0x41030>;
+                clock-names = "apb_pclk";
+                clocks = <0x18>;
+                compatible = "arm,primecell";
+                interrupts = <0x00 0x01 0x04>;
+                reg = <0x00 0x2000 0x00 0x1000>;
+        };
+
+        timer {
+                always-on;
+                compatible = "arm,armv8-timer";
+                interrupts = <0x01 0x0d 0x108 0x01 0x0e 0x108 0x01 0x0b 0x108 0x01 0x0a 0x108>;
+        };
+
+        vmwdt@3000 {
+                clock-frequency = <0x02>;
+                compatible = "qemu,vcpu-stall-detector";
+                interrupts = <0x01 0x0f 0x101>;
+                reg = <0x00 0x3000 0x00 0x1000>;
+                timeout-sec = <0x0a>;
+        };
+};
diff --git a/tests/testapk/AndroidTest.xml b/tests/testapk/AndroidTest.xml
index 22cd0dc..e490da4 100644
--- a/tests/testapk/AndroidTest.xml
+++ b/tests/testapk/AndroidTest.xml
@@ -39,4 +39,8 @@
         <option name="shell-timeout" value="300000" />
         <option name="test-timeout" value="300000" />
     </test>
+
+    <!-- Controller that will skip the module if a native bridge situation is detected -->
+    <!-- For example: module wants to run arm and device is x86 -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.NativeBridgeModuleController" />
 </configuration>
diff --git a/tests/vm_accessor/README.md b/tests/vm_accessor/README.md
index c85cf3c..8b0eb2a 100644
--- a/tests/vm_accessor/README.md
+++ b/tests/vm_accessor/README.md
@@ -1,15 +1,16 @@
 # Demo for serving a service in a VM
 
 You can implement a service in a VM, and let client in the Android can use it
-as if it's in the Android. To do so, implement IAccessor.
+as if it's in the Android. To do so, use libbinder's IAccessor.
 
-IAccessor allows AIDL service in a VM can be accessed via servicemanager.
-To do so, VM owners should also provide IAccessor implementation. servicemanager
-will connect to the IAccessor and get the binder of the service in a VM with it.
+IAccessor allows AIDL service in a VM to be accessed via servicemanager.
+To do so, VM owners should also provide IAccessor through libbinder's service
+manager APIs. servicemanager will connect to the IAccessor and get the binder
+of the service in a VM with it.
 
 com.android.virt.accessor_demo apex contains the minimum setup for IAccessor as
 follows:
-  - accessor_demo: Sample implementation of IAccessor, which is expected to
+  - accessor_demo: Sample implementation using IAccessor, which is expected to
       launch VM and returns the Vsock connection of service in the VM.
   - AccessorVmApp: Sample app that conatins VM payload. Provides the actual
       implementation of service in a VM.
diff --git a/tests/vm_accessor/accessor/Android.bp b/tests/vm_accessor/accessor/Android.bp
index 7c0ee6d..8055f91 100644
--- a/tests/vm_accessor/accessor/Android.bp
+++ b/tests/vm_accessor/accessor/Android.bp
@@ -14,7 +14,6 @@
     ],
     rustlibs: [
         "android.system.virtualizationservice-rust",
-        "android.os.accessor-rust",
         "libanyhow",
         "libandroid_logger",
         "libbinder_rs",
diff --git a/tests/vm_accessor/accessor/src/accessor.rs b/tests/vm_accessor/accessor/src/accessor.rs
deleted file mode 100644
index 966bffb..0000000
--- a/tests/vm_accessor/accessor/src/accessor.rs
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright 2024, The Android Open Source Project
-//
-// 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.
-
-//! IAcessor implementation.
-//! TODO: Keep this in proper places, so other pVMs can use this.
-//! TODO: Allows to customize VMs for launching. (e.g. port, ...)
-
-use android_os_accessor::aidl::android::os::IAccessor::IAccessor;
-use binder::{self, Interface, ParcelFileDescriptor};
-use log::info;
-use std::time::Duration;
-use vmclient::VmInstance;
-
-// Note: Do not use LazyServiceGuard here, to make this service and VM are quit
-//       when nobody references it.
-// TODO(b/353492849): Do not use IAccessor directly.
-#[derive(Debug)]
-pub struct Accessor {
-    // Note: we can't simply keep reference by specifying lifetime to Accessor,
-    //       because 'trait Interface' requires 'static.
-    vm: VmInstance,
-    port: i32,
-    instance: String,
-}
-
-impl Accessor {
-    pub fn new(vm: VmInstance, port: i32, instance: &str) -> Self {
-        Self { vm, port, instance: instance.into() }
-    }
-}
-
-impl Interface for Accessor {}
-
-impl IAccessor for Accessor {
-    fn addConnection(&self) -> binder::Result<ParcelFileDescriptor> {
-        self.vm.wait_until_ready(Duration::from_secs(20)).unwrap();
-
-        info!("VM is ready. Connecting to service via port {}", self.port);
-
-        self.vm.vm.connectVsock(self.port)
-    }
-    fn getInstanceName(&self) -> binder::Result<String> {
-        Ok(self.instance.clone())
-    }
-}
diff --git a/tests/vm_accessor/accessor/src/main.rs b/tests/vm_accessor/accessor/src/main.rs
index 49f5794..db53d8e 100644
--- a/tests/vm_accessor/accessor/src/main.rs
+++ b/tests/vm_accessor/accessor/src/main.rs
@@ -14,16 +14,14 @@
 
 //! Android VM control tool.
 
-mod accessor;
 mod run;
 
-use accessor::Accessor;
-use android_os_accessor::aidl::android::os::IAccessor::BnAccessor;
 use anyhow::Error;
 use anyhow::{anyhow, bail};
-use binder::{BinderFeatures, ProcessState};
+use binder::ProcessState;
 use log::info;
 use run::run_vm;
+use std::time::Duration;
 
 // Private contract between IAccessor impl and VM service.
 const PORT: i32 = 5678;
@@ -40,11 +38,13 @@
     );
 
     let vm = run_vm()?;
+    vm.wait_until_ready(Duration::from_secs(20)).unwrap();
+    let accessor = vm.vm.createAccessorBinder(SERVICE_NAME, PORT).unwrap();
+
+    let accessor_delegator = binder::delegate_accessor(SERVICE_NAME, accessor).unwrap();
 
     // If you want to serve multiple services in a VM, then register Accessor impls multiple times.
-    let accessor = Accessor::new(vm, PORT, SERVICE_NAME);
-    let accessor_binder = BnAccessor::new_binder(accessor, BinderFeatures::default());
-    binder::register_lazy_service(SERVICE_NAME, accessor_binder.as_binder()).map_err(|e| {
+    binder::register_lazy_service(SERVICE_NAME, accessor_delegator).map_err(|e| {
         anyhow!("Failed to register lazy service, service={SERVICE_NAME}, err={e:?}",)
     })?;
     info!("service {SERVICE_NAME} is registered as lazy service");
diff --git a/tests/vm_accessor/accessor/src/run.rs b/tests/vm_accessor/accessor/src/run.rs
index 932baab..6dcc507 100644
--- a/tests/vm_accessor/accessor/src/run.rs
+++ b/tests/vm_accessor/accessor/src/run.rs
@@ -128,6 +128,7 @@
         Some(android_log_fd()?), /* console_out */
         None,                    /* console_in */
         Some(android_log_fd()?), /* log */
+        None,                    /* dump_dt */
         Some(Box::new(Callback {})),
     )
     .context("Failed to create VM")?;
diff --git a/tests/vmbase_example/src/main.rs b/tests/vmbase_example/src/main.rs
index e0563b7..34a2b0b 100644
--- a/tests/vmbase_example/src/main.rs
+++ b/tests/vmbase_example/src/main.rs
@@ -119,6 +119,7 @@
         Some(console),
         /* consoleIn */ None,
         Some(log_writer),
+        /* dump_dt */ None,
         None,
     )
     .context("Failed to create VM")?;