Merge changes I83ba378b,Ice701926 into main

* changes:
  Remove avf_v_test_apis aconfig flag
  Remove reference to android14-6.1 microdroid kernel
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index 932ca76..5e45c02 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -11,6 +11,7 @@
     resource_dirs: ["res"],
     asset_dirs: ["assets"],
     static_libs: [
+        "VmTerminalApp.aidl-java",
         "vm_launcher_lib",
         "androidx-constraintlayout_constraintlayout",
         "com.google.android.material_material",
@@ -26,3 +27,18 @@
         "com.android.virt",
     ],
 }
+
+aidl_interface {
+    name: "VmTerminalApp.aidl",
+    srcs: ["aidl/**/*.aidl"],
+    unstable: true,
+    local_include_dir: "aidl",
+    backend: {
+        java: {
+            enabled: true,
+            apex_available: [
+                "com.android.virt",
+            ],
+        },
+    },
+}
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index 28b5436..61737fe 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -3,6 +3,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     package="com.android.virtualization.terminal">
 
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <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" />
@@ -55,6 +56,11 @@
                 <action android:name="android.intent.action.MAIN" />
             </intent-filter>
         </activity>
+        <service android:name=".InstallerService"
+            android:foregroundServiceType="specialUse"
+            android:value="Prepares Linux image"
+            android:exported="false"
+            android:stopWithTask="true" />
 
         <service
             android:name="com.android.virtualization.vmlauncher.VmLauncherService"
diff --git a/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallProgressListener.aidl b/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallProgressListener.aidl
new file mode 100644
index 0000000..94e33d9
--- /dev/null
+++ b/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallProgressListener.aidl
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+// TODO(b/374015561): Provide progress update
+oneway interface IInstallProgressListener {
+    void onCompleted();
+    void onError(in String displayText);
+}
diff --git a/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallerService.aidl b/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallerService.aidl
new file mode 100644
index 0000000..daf1fa4
--- /dev/null
+++ b/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallerService.aidl
@@ -0,0 +1,27 @@
+/*
+ * 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 com.android.virtualization.terminal.IInstallProgressListener;
+
+interface IInstallerService {
+    void requestInstall();
+    void setProgressListener(in IInstallProgressListener listener);
+
+    boolean isInstalling();
+    boolean isInstalled();
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.java
new file mode 100644
index 0000000..66552d5
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.java
@@ -0,0 +1,38 @@
+/*
+ * 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.Manifest;
+import android.content.pm.PackageManager;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+public abstract class BaseActivity extends AppCompatActivity {
+    private static final int POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE = 101;
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        if (getApplicationContext().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
+                != PackageManager.PERMISSION_GRANTED) {
+            requestPermissions(
+                    new String[] {Manifest.permission.POST_NOTIFICATIONS},
+                    POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE);
+        }
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
index a49ea72..428fd91 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
@@ -16,56 +16,235 @@
 
 package com.android.virtualization.terminal;
 
-import android.app.Activity;
+import android.annotation.MainThread;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.FileUtils;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.text.format.Formatter;
 import android.util.Log;
+import android.widget.CheckBox;
 import android.widget.TextView;
+import android.widget.Toast;
 
-import com.android.virtualization.vmlauncher.InstallUtils;
-
+import java.lang.ref.WeakReference;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 
-public class InstallerActivity extends Activity {
+public class InstallerActivity extends BaseActivity {
     private static final String TAG = "LinuxInstaller";
 
-    ExecutorService executorService = Executors.newSingleThreadExecutor();
+    private static final long ESTIMATED_IMG_SIZE_BYTES = FileUtils.parseSize("350MB");
+
+    private ExecutorService mExecutorService;
+    private CheckBox mWaitForWifiCheckbox;
+    private TextView mInstallButton;
+
+    private IInstallerService mService;
+    private ServiceConnection mInstallerServiceConnection;
+    private InstallProgressListener mInstallProgressListener;
+    private boolean mInstallRequested;
 
     @Override
-    protected void onCreate(Bundle savedInstanceState) {
+    public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setResult(RESULT_CANCELED);
 
+        mInstallProgressListener = new InstallProgressListener(this);
+
         setContentView(R.layout.activity_installer);
 
-        executorService.execute(this::installLinuxImage);
+        TextView desc = (TextView) findViewById(R.id.installer_desc);
+        desc.setText(
+                getString(
+                        R.string.installer_desc_text_format,
+                        Formatter.formatShortFileSize(this, ESTIMATED_IMG_SIZE_BYTES)));
+
+        mWaitForWifiCheckbox = (CheckBox) findViewById(R.id.installer_wait_for_wifi_checkbox);
+        mInstallButton = (TextView) findViewById(R.id.installer_install_button);
+
+        mInstallButton.setOnClickListener(
+                (event) -> {
+                    requestInstall();
+                });
+
+        Intent intent = new Intent(this, InstallerService.class);
+        mInstallerServiceConnection = new InstallerServiceConnection(this);
+        if (!bindService(intent, mInstallerServiceConnection, Context.BIND_AUTO_CREATE)) {
+            handleCriticalError(new Exception("Failed to connect to installer service"));
+        }
     }
 
-    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");
+    @Override
+    public void onDestroy() {
+        if (mInstallerServiceConnection != null) {
+            unbindService(mInstallerServiceConnection);
+            mInstallerServiceConnection = null;
         }
-        setResult(RESULT_CANCELED, null);
+
+        super.onDestroy();
+    }
+
+    public void handleCriticalError(Exception e) {
+        if (Build.isDebuggable()) {
+            Toast.makeText(
+                            this,
+                            e.getMessage() + ". File a bugreport to go/ferrochrome-bug",
+                            Toast.LENGTH_LONG)
+                    .show();
+        }
+        Log.e(TAG, "Internal error", e);
+        finishWithResult(RESULT_CANCELED);
+    }
+
+    private void finishWithResult(int resultCode) {
+        setResult(resultCode);
         finish();
     }
 
-    private void updateStatus(String line) {
-        runOnUiThread(
-                () -> {
-                    TextView statusView = findViewById(R.id.status_txt_view);
-                    statusView.append(line + "\n");
-                });
+    private void setInstallEnabled(boolean enable) {
+        mInstallButton.setEnabled(enable);
+        mWaitForWifiCheckbox.setEnabled(enable);
+
+        int resId =
+                enable
+                        ? R.string.installer_install_button_enabled_text
+                        : R.string.installer_install_button_disabled_text;
+        mInstallButton.setText(getString(resId));
+    }
+
+    @MainThread
+    private void requestInstall() {
+        setInstallEnabled(/* enable= */ false);
+
+        if (mService != null) {
+            try {
+                mService.requestInstall();
+            } catch (RemoteException e) {
+                handleCriticalError(e);
+            }
+        } else {
+            Log.d(TAG, "requestInstall() is called, but not yet connected");
+            mInstallRequested = true;
+        }
+    }
+
+    @MainThread
+    public void handleInstallerServiceConnected() {
+        try {
+            mService.setProgressListener(mInstallProgressListener);
+            if (mService.isInstalled()) {
+                // Finishing this activity will trigger MainActivity::onResume(),
+                // and VM will be started from there.
+                finishWithResult(RESULT_OK);
+                return;
+            }
+
+            if (mInstallRequested) {
+                requestInstall();
+            } else if (mService.isInstalling()) {
+                setInstallEnabled(false);
+            }
+        } catch (RemoteException e) {
+            handleCriticalError(e);
+        }
+    }
+
+    @MainThread
+    public void handleInstallerServiceDisconnected() {
+        handleCriticalError(new Exception("InstallerService is destroyed while in use"));
+    }
+
+    @MainThread
+    private void handleError(String displayText) {
+        // TODO(b/375542145): Display error with snackbar.
+        if (Build.isDebuggable()) {
+            Toast.makeText(this, displayText, Toast.LENGTH_LONG).show();
+        }
+        setInstallEnabled(true);
+    }
+
+    private static class InstallProgressListener extends IInstallProgressListener.Stub {
+        private final WeakReference<InstallerActivity> mActivity;
+
+        InstallProgressListener(InstallerActivity activity) {
+            mActivity = new WeakReference<>(activity);
+        }
+
+        @Override
+        public void onCompleted() {
+            InstallerActivity activity = mActivity.get();
+            if (activity == null) {
+                // Ignore incoming connection or disconnection after activity is destroyed.
+                return;
+            }
+
+            // MainActivity will be resume and handle rest of progress.
+            activity.finishWithResult(RESULT_OK);
+        }
+
+        @Override
+        public void onError(String displayText) {
+            InstallerActivity context = mActivity.get();
+            if (context == null) {
+                // Ignore incoming connection or disconnection after activity is destroyed.
+                return;
+            }
+
+            context.runOnUiThread(
+                    () -> {
+                        InstallerActivity activity = mActivity.get();
+                        if (activity == null) {
+                            // Ignore incoming connection or disconnection after activity is
+                            // destroyed.
+                            return;
+                        }
+
+                        activity.handleError(displayText);
+                    });
+        }
+    }
+
+    @MainThread
+    public static final class InstallerServiceConnection implements ServiceConnection {
+        private final WeakReference<InstallerActivity> mActivity;
+
+        InstallerServiceConnection(InstallerActivity activity) {
+            mActivity = new WeakReference<>(activity);
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            InstallerActivity activity = mActivity.get();
+            if (activity == null || activity.mInstallerServiceConnection == null) {
+                // Ignore incoming connection or disconnection after activity is destroyed.
+                return;
+            }
+            if (service == null) {
+                activity.handleCriticalError(new Exception("service shouldn't be null"));
+            }
+
+            activity.mService = IInstallerService.Stub.asInterface(service);
+            activity.handleInstallerServiceConnected();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            InstallerActivity activity = mActivity.get();
+            if (activity == null || activity.mInstallerServiceConnection == null) {
+                // Ignore incoming connection or disconnection after activity is destroyed.
+                return;
+            }
+
+            if (activity.mInstallerServiceConnection != null) {
+                activity.unbindService(activity.mInstallerServiceConnection);
+                activity.mInstallerServiceConnection = null;
+            }
+            activity.handleInstallerServiceDisconnected();
+        }
     }
 }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
new file mode 100644
index 0000000..b3102db
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
@@ -0,0 +1,287 @@
+/*
+ * 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.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.os.IBinder;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.virtualization.vmlauncher.InstallUtils;
+
+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.IOException;
+import java.lang.ref.WeakReference;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class InstallerService extends Service {
+    private static final String TAG = "InstallerService";
+
+    private static final String NOTIFICATION_CHANNEL_ID = "installer";
+    private static final int NOTIFICATION_ID = 1313; // any unique number among notifications
+
+    // TODO(b/369740847): Replace this URL with dl.google.com
+    private static final String IMAGE_URL =
+            "https://github.com/ikicha/debian_ci/releases/download/first/images.tar.gz";
+
+    private final Object mLock = new Object();
+
+    private Notification mNotification;
+
+    @GuardedBy("mLock")
+    private boolean mIsInstalling;
+
+    @GuardedBy("mLock")
+    private IInstallProgressListener mListener;
+
+    private ExecutorService mExecutorService;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        // Create mandatory notification
+        NotificationManager manager = getSystemService(NotificationManager.class);
+        if (manager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) == null) {
+            NotificationChannel channel =
+                    new NotificationChannel(
+                            NOTIFICATION_CHANNEL_ID,
+                            getString(R.string.installer_notif_title_text),
+                            NotificationManager.IMPORTANCE_DEFAULT);
+            manager.createNotificationChannel(channel);
+        }
+
+        Intent intent = new Intent(this, MainActivity.class);
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(
+                        this, /* requestCode= */ 0, intent, PendingIntent.FLAG_IMMUTABLE);
+        mNotification =
+                new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
+                        .setSmallIcon(R.drawable.ic_launcher_foreground)
+                        .setContentTitle(getString(R.string.installer_notif_title_text))
+                        .setContentText(getString(R.string.installer_notif_desc_text))
+                        .setOngoing(true)
+                        .setContentIntent(pendingIntent)
+                        .build();
+
+        mExecutorService = Executors.newSingleThreadExecutor();
+    }
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        return new InstallerServiceImpl(this);
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        super.onStartCommand(intent, flags, startId);
+
+        Log.d(TAG, "Starting service ...");
+
+        return START_STICKY;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+
+        Log.d(TAG, "Service is destroyed");
+        if (mExecutorService != null) {
+            mExecutorService.shutdown();
+        }
+    }
+
+    private void requestInstall() {
+        Log.i(TAG, "Installing..");
+
+        // Make service to be long running, even after unbind() when InstallerActivity is destroyed
+        // The service will still be destroyed if task is remove.
+        startService(new Intent(this, InstallerService.class));
+        startForeground(
+                NOTIFICATION_ID, mNotification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
+        synchronized (mLock) {
+            mIsInstalling = true;
+        }
+
+        mExecutorService.execute(
+                () -> {
+                    // TODO(b/374015561): Provide progress update
+                    boolean success = downloadFromSdcard() || downloadFromUrl();
+
+                    stopForeground(STOP_FOREGROUND_REMOVE);
+
+                    synchronized (mLock) {
+                        mIsInstalling = false;
+                    }
+                    if (success) {
+                        notifyCompleted();
+                    }
+                });
+    }
+
+    private boolean downloadFromSdcard() {
+        // Installing from sdcard is preferred, but only supported only in debuggable build.
+        if (Build.isDebuggable()) {
+            Log.i(TAG, "trying to install /sdcard/linux/images.tar.gz");
+
+            if (InstallUtils.installImageFromExternalStorage(this)) {
+                Log.i(TAG, "image is installed from /sdcard/linux/images.tar.gz");
+                return true;
+            }
+            Log.i(TAG, "Failed to install /sdcard/linux/images.tar.gz");
+        } else {
+            Log.i(TAG, "Non-debuggable build doesn't support installation from /sdcard/linux");
+        }
+        return false;
+    }
+
+    // TODO(b/374015561): Support pause/resume download
+    // TODO(b/374015561): Wait for Wi-Fi on metered network if requested.
+    private boolean downloadFromUrl() {
+        Log.i(TAG, "trying to download from " + IMAGE_URL);
+
+        try (BufferedInputStream inputStream =
+                        new BufferedInputStream(new URL(IMAGE_URL).openStream());
+                TarArchiveInputStream tar =
+                        new TarArchiveInputStream(new GzipCompressorInputStream(inputStream))) {
+            ArchiveEntry entry;
+            Path baseDir = InstallUtils.getInternalStorageDir(this).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 (UnknownHostException e) {
+            // Log.e() doesn't print stack trace for UnknownHostException
+            Log.e(TAG, "Install failed UnknownHostException: " + e.getMessage());
+            notifyError(getString(R.string.installer_install_network_error_message));
+            return false;
+        } catch (IOException e) {
+            // TODO(b/374015561): Provide more finer grained error message
+            Log.e(TAG, "Installation failed", e);
+            notifyError(getString(R.string.installer_error_unknown));
+            return false;
+        }
+
+        if (!InstallUtils.resolvePathInVmConfig(this)) {
+            // TODO(b/374015561): Provide more finer grained error message
+            notifyError(getString(R.string.installer_error_unknown));
+            return false;
+        }
+        return true;
+    }
+
+    private void notifyError(String displayText) {
+        IInstallProgressListener listener;
+        synchronized (mLock) {
+            listener = mListener;
+        }
+
+        try {
+            listener.onError(displayText);
+        } catch (Exception e) {
+            // ignore. Activity may not exist.
+        }
+    }
+
+    private void notifyCompleted() {
+        IInstallProgressListener listener;
+        synchronized (mLock) {
+            listener = mListener;
+        }
+
+        try {
+            listener.onCompleted();
+        } catch (Exception e) {
+            // ignore. Activity may not exist.
+        }
+    }
+
+    private static final class InstallerServiceImpl extends IInstallerService.Stub {
+        // Holds weak reference to avoid Context leak
+        private final WeakReference<InstallerService> mService;
+
+        public InstallerServiceImpl(InstallerService service) {
+            mService = new WeakReference<>(service);
+        }
+
+        private InstallerService ensureServiceConnected() throws RuntimeException {
+            InstallerService service = mService.get();
+            if (service == null) {
+                throw new RuntimeException(
+                        "Internal error: Installer service is being accessed after destroyed");
+            }
+            return service;
+        }
+
+        @Override
+        public void requestInstall() {
+            InstallerService service = ensureServiceConnected();
+            synchronized (service.mLock) {
+                service.requestInstall();
+            }
+        }
+
+        @Override
+        public void setProgressListener(IInstallProgressListener listener) {
+            InstallerService service = ensureServiceConnected();
+            synchronized (service.mLock) {
+                service.mListener = listener;
+            }
+        }
+
+        @Override
+        public boolean isInstalling() {
+            InstallerService service = ensureServiceConnected();
+            synchronized (service.mLock) {
+                return service.mIsInstalling;
+            }
+        }
+
+        @Override
+        public boolean isInstalled() {
+            InstallerService service = ensureServiceConnected();
+            synchronized (service.mLock) {
+                return !service.mIsInstalling && InstallUtils.isImageInstalled(service);
+            }
+        }
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index 0a750e3..437e0be 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -15,19 +15,16 @@
  */
 package com.android.virtualization.terminal;
 
-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.content.res.Configuration;
 import android.graphics.drawable.Icon;
 import android.graphics.fonts.FontStyle;
 import android.net.http.SslError;
-import android.os.Build;
 import android.os.Bundle;
 import android.system.ErrnoException;
 import android.system.Os;
@@ -45,8 +42,6 @@
 import android.webkit.WebViewClient;
 import android.widget.Toast;
 
-import androidx.appcompat.app.AppCompatActivity;
-
 import com.android.virtualization.vmlauncher.InstallUtils;
 import com.android.virtualization.vmlauncher.VmLauncherServices;
 
@@ -68,7 +63,7 @@
 import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
 
-public class MainActivity extends AppCompatActivity
+public class MainActivity extends BaseActivity
         implements VmLauncherServices.VmLauncherServiceCallback,
                 AccessibilityManager.TouchExplorationStateChangeListener {
 
@@ -99,13 +94,12 @@
                     .show();
         }
 
-        checkAndRequestPostNotificationsPermission();
-
         NotificationManager notificationManager = getSystemService(NotificationManager.class);
-        NotificationChannel notificationChannel =
-                new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_LOW);
-        assert notificationManager != null;
-        notificationManager.createNotificationChannel(notificationChannel);
+        if (notificationManager.getNotificationChannel(TAG) == null) {
+            NotificationChannel notificationChannel =
+                    new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_LOW);
+            notificationManager.createNotificationChannel(notificationChannel);
+        }
 
         setContentView(R.layout.activity_headless);
 
@@ -338,15 +332,6 @@
         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);
@@ -416,8 +401,7 @@
     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)) {
+        if (!InstallUtils.isImageInstalled(this)) {
             Intent intent = new Intent(this, InstallerActivity.class);
             startActivityForResult(intent, REQUEST_CODE_INSTALLER);
             return true;
diff --git a/android/TerminalApp/res/drawable/ic_lock_open.xml b/android/TerminalApp/res/drawable/ic_lock_open.xml
new file mode 100644
index 0000000..c623592
--- /dev/null
+++ b/android/TerminalApp/res/drawable/ic_lock_open.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M220,326L610,326L610,230Q610,175.83 572.12,137.92Q534.24,100 480.12,100Q426,100 388,137.92Q350,175.83 350,230L290,230Q290,151 345.61,95.5Q401.21,40 480.11,40Q559,40 614.5,95.58Q670,151.15 670,230L670,326L740,326Q764.75,326 782.38,343.62Q800,361.25 800,386L800,820Q800,844.75 782.38,862.37Q764.75,880 740,880L220,880Q195.25,880 177.63,862.37Q160,844.75 160,820L160,386Q160,361.25 177.63,343.62Q195.25,326 220,326ZM220,820L740,820Q740,820 740,820Q740,820 740,820L740,386Q740,386 740,386Q740,386 740,386L220,386Q220,386 220,386Q220,386 220,386L220,820Q220,820 220,820Q220,820 220,820ZM480.17,680Q512,680 534.5,657.97Q557,635.94 557,605Q557,575 534.33,550.5Q511.66,526 479.83,526Q448,526 425.5,550.5Q403,575 403,605.5Q403,636 425.67,658Q448.34,680 480.17,680ZM220,820Q220,820 220,820Q220,820 220,820L220,386Q220,386 220,386Q220,386 220,386L220,386Q220,386 220,386Q220,386 220,386L220,820Q220,820 220,820Q220,820 220,820Z"/>
+</vector>
+
diff --git a/android/TerminalApp/res/layout/activity_installer.xml b/android/TerminalApp/res/layout/activity_installer.xml
index 3967167..c375cb8 100644
--- a/android/TerminalApp/res/layout/activity_installer.xml
+++ b/android/TerminalApp/res/layout/activity_installer.xml
@@ -6,10 +6,51 @@
     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"/>
+    <TextView
+        android:id="@+id/installer_title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentTop="true"
+        android:layout_alignParentStart="true"
+        android:textSize="32sp"
+        android:text="@string/installer_title_text" />
+
+    <ImageView
+        android:id="@+id/installer_icon"
+        android:layout_width="match_parent"
+        android:layout_height="300dp"
+        android:padding="10dp"
+        android:layout_below="@id/installer_title"
+        android:layout_alignParentStart="true"
+        android:src="@drawable/ic_lock_open"
+        android:adjustViewBounds="true" />
+
+    <TextView
+        android:id="@+id/installer_desc"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="10dp"
+        android:layout_below="@id/installer_icon"
+        android:layout_alignParentStart="true"
+        android:singleLine="false"
+        android:textSize="24sp" />
+
+    <CheckBox
+        android:id="@+id/installer_wait_for_wifi_checkbox"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="10dp"
+        android:layout_alignParentEnd="true"
+        android:layout_below="@id/installer_desc"
+        android:text="@string/installer_wait_for_wifi_checkbox_text" />
+
+    <Button
+        android:id="@+id/installer_install_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="10dp"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentEnd="true"
+        android:text="@string/installer_install_button_enabled_text" />
 
 </RelativeLayout>
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
index 0cdb939..f8350a0 100644
--- a/android/TerminalApp/res/values/strings.xml
+++ b/android/TerminalApp/res/values/strings.xml
@@ -20,6 +20,27 @@
     <!-- 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>
 
+    <!-- Installer activity title [CHAR LIMIT=none] -->
+    <string name="installer_title_text">Install Linux terminal</string>
+    <!-- Installer activity description format [CHAR LIMIT=none] -->
+    <string name="installer_desc_text_format">To launch Linux terminal, you need to download roughly <xliff:g id="expected_size" example="350GB">%1$s</xliff:g> of data over network.\nWould you proceed?</string>
+    <!-- Checkbox at the installer activity to wait for Wi-Fi on metered network to prevent from paying network traffic [CHAR LIMIT=none] -->
+    <string name="installer_wait_for_wifi_checkbox_text">Wait for Wi-Fi on metered network</string>
+    <!-- Button at the installer activity to confirm installation [CHAR LIMIT=16] -->
+    <string name="installer_install_button_enabled_text">Install</string>
+    <!-- Button at the installer activity to when installation is already in progress [CHAR LIMIT=16] -->
+    <string name="installer_install_button_disabled_text">Installing</string>
+    <!-- Toast message at installer activity when network doesn't meet[CHAR LIMIT=none] -->
+    <string name="installer_install_network_error_message">Network error. Check connection and retry.</string>
+    <!-- Notification title for installer [CHAR LIMIT=64] -->
+    <string name="installer_notif_title_text">Installing Linux terminal</string>
+    <!-- Notification description for installer [CHAR LIMIT=none] -->
+    <string name="installer_notif_desc_text">Linux terminal will be started after finish</string>
+    <!-- Toast error message for install failure due to the network issue [CHAR LIMIT=none] -->
+    <string name="installer_error_network">Failed to install due to the network issue</string>
+    <!-- Toast error message for install failure due to the unidentified issue [CHAR LIMIT=none] -->
+    <string name="installer_error_unknown">Failed to install. Try again.</string>
+
     <!-- Action bar icon name for the settings view CHAR LIMIT=none] -->
     <string name="action_settings">Settings</string>
 
diff --git a/android/virtmgr/Android.bp b/android/virtmgr/Android.bp
index d0d7915..ad63995 100644
--- a/android/virtmgr/Android.bp
+++ b/android/virtmgr/Android.bp
@@ -37,6 +37,7 @@
         "libbinder_rs",
         "libcfg_if",
         "libclap",
+        "libcrosvm_control_static",
         "libcstr",
         "libcommand_fds",
         "libdisk",
@@ -62,7 +63,6 @@
         "libstatslog_virtualization_rust",
         "libtombstoned_client_rust",
         "libvbmeta_rust",
-        "libvm_control",
         "libvmconfig",
         "libzip",
         "libvsock",
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index 52bfd87..5dac07f 100644
--- a/android/virtmgr/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -17,7 +17,7 @@
 use crate::{get_calling_pid, get_calling_uid, get_this_pid};
 use crate::atom::{write_vm_booted_stats, write_vm_creation_stats};
 use crate::composite::make_composite_image;
-use crate::crosvm::{AudioConfig, CrosvmConfig, DiskFile, DisplayConfig, GpuConfig, InputDeviceOption, PayloadState, UsbConfig, VmContext, VmInstance, VmState};
+use crate::crosvm::{AudioConfig, CrosvmConfig, DiskFile, SharedPathConfig, DisplayConfig, GpuConfig, InputDeviceOption, PayloadState, UsbConfig, VmContext, VmInstance, VmState};
 use crate::debug_config::DebugConfig;
 use crate::dt_overlay::{create_device_tree_overlay, VM_DT_OVERLAY_MAX_SIZE, VM_DT_OVERLAY_PATH};
 use crate::payload::{add_microdroid_payload_images, add_microdroid_system_images, add_microdroid_vendor_image};
@@ -32,6 +32,7 @@
     AssignableDevice::AssignableDevice,
     CpuTopology::CpuTopology,
     DiskImage::DiskImage,
+    SharedPath::SharedPath,
     InputDevice::InputDevice,
     IVirtualMachine::{self, BnVirtualMachine},
     IVirtualMachineCallback::IVirtualMachineCallback,
@@ -613,6 +614,8 @@
             })
             .collect::<Result<Vec<DiskFile>, _>>()?;
 
+        let shared_paths = assemble_shared_paths(&config.sharedPaths, &temporary_directory)?;
+
         let (cpus, host_cpu_topology) = match config.cpuTopology {
             CpuTopology::MATCH_HOST => (None, true),
             CpuTopology::ONE_CPU => (NonZeroU32::new(1), false),
@@ -719,6 +722,7 @@
             kernel,
             initrd,
             disks,
+            shared_paths,
             params: config.params.to_owned(),
             protected: *is_protected,
             debug_config,
@@ -956,6 +960,32 @@
         },
     })
 }
+
+fn assemble_shared_paths(
+    shared_paths: &[SharedPath],
+    temporary_directory: &Path,
+) -> Result<Vec<SharedPathConfig>, Status> {
+    if shared_paths.is_empty() {
+        return Ok(Vec::new()); // Return an empty vector if shared_paths is empty
+    }
+
+    shared_paths
+        .iter()
+        .map(|path| {
+            Ok(SharedPathConfig {
+                path: path.sharedPath.clone(),
+                host_uid: path.hostUid,
+                host_gid: path.hostGid,
+                guest_uid: path.guestUid,
+                guest_gid: path.guestGid,
+                mask: path.mask,
+                tag: path.tag.clone(),
+                socket_path: temporary_directory.join(&path.socket).to_string_lossy().to_string(),
+            })
+        })
+        .collect()
+}
+
 /// Given the configuration for a disk image, assembles the `DiskFile` to pass to crosvm.
 ///
 /// This may involve assembling a composite disk from a set of partition images.
diff --git a/android/virtmgr/src/crosvm.rs b/android/virtmgr/src/crosvm.rs
index 25271f8..86b3571 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -29,12 +29,14 @@
 use shared_child::SharedChild;
 use std::borrow::Cow;
 use std::cmp::max;
+use std::ffi::CString;
 use std::fmt;
 use std::fs::{read_to_string, File};
 use std::io::{self, Read};
 use std::mem;
 use std::num::{NonZeroU16, NonZeroU32};
 use std::os::unix::io::{AsRawFd, OwnedFd};
+use std::os::unix::process::CommandExt;
 use std::os::unix::process::ExitStatusExt;
 use std::path::{Path, PathBuf};
 use std::process::{Command, ExitStatus};
@@ -56,9 +58,6 @@
 use tombstoned_client::{TombstonedConnection, DebuggerdDumpType};
 use rpcbinder::RpcServer;
 
-/// external/crosvm
-use vm_control::{BalloonControlCommand, VmRequest, VmResponse};
-
 const CROSVM_PATH: &str = "/apex/com.android.virt/bin/crosvm";
 
 /// Version of the platform that crosvm currently implements. The format follows SemVer. This
@@ -108,6 +107,7 @@
     pub kernel: Option<File>,
     pub initrd: Option<File>,
     pub disks: Vec<DiskFile>,
+    pub shared_paths: Vec<SharedPathConfig>,
     pub params: Option<String>,
     pub protected: bool,
     pub debug_config: DebugConfig,
@@ -224,6 +224,19 @@
     pub writable: bool,
 }
 
+/// Shared path between host and guest VM.
+#[derive(Debug)]
+pub struct SharedPathConfig {
+    pub path: String,
+    pub host_uid: i32,
+    pub host_gid: i32,
+    pub guest_uid: i32,
+    pub guest_gid: i32,
+    pub mask: i32,
+    pub tag: String,
+    pub socket_path: String,
+}
+
 /// virtio-input device configuration from `external/crosvm/src/crosvm/config.rs`
 #[derive(Debug)]
 #[allow(dead_code)]
@@ -306,6 +319,8 @@
             let tap =
                 if let Some(tap_file) = &config.tap { Some(tap_file.try_clone()?) } else { None };
 
+            run_virtiofs(&config)?;
+
             // If this fails and returns an error, `self` will be left in the `Failed` state.
             let child =
                 Arc::new(run_vm(config, &instance.crosvm_control_socket_path, failure_pipe_write)?);
@@ -638,37 +653,34 @@
         Ok(())
     }
 
-    /// Responds to memory-trimming notifications by inflating the virtio
-    /// balloon to reclaim guest memory.
+    /// Returns current virtio-balloon size.
     pub fn get_memory_balloon(&self) -> Result<u64, Error> {
-        let request = VmRequest::BalloonCommand(BalloonControlCommand::Stats {});
-        let result =
-            match vm_control::client::handle_request(&request, &self.crosvm_control_socket_path) {
-                Ok(VmResponse::BalloonStats { stats: _, balloon_actual }) => balloon_actual,
-                Ok(VmResponse::Err(e)) => {
-                    // ENOTSUP is returned when the balloon protocol is not initialized. This
-                    // can occur for numerous reasons: Guest is still booting, guest doesn't
-                    // support ballooning, host doesn't support ballooning. We don't log or
-                    // raise an error in this case: trim is just a hint and we can ignore it.
-                    if e.errno() != libc::ENOTSUP {
-                        bail!("Errno return when requesting balloon stats: {}", e.errno())
-                    }
-                    0
-                }
-                e => bail!("Error requesting balloon stats: {:?}", e),
-            };
-        Ok(result)
+        let socket_path_cstring = path_to_cstring(&self.crosvm_control_socket_path);
+        let mut balloon_actual = 0u64;
+        // SAFETY: Pointers are valid for the lifetime of the call. Null `stats` is valid.
+        let success = unsafe {
+            crosvm_control::crosvm_client_balloon_stats(
+                socket_path_cstring.as_ptr(),
+                /* stats= */ std::ptr::null_mut(),
+                &mut balloon_actual,
+            )
+        };
+        if !success {
+            bail!("Error requesting balloon stats");
+        }
+        Ok(balloon_actual)
     }
 
-    /// Responds to memory-trimming notifications by inflating the virtio
-    /// balloon to reclaim guest memory.
+    /// Inflates the virtio-balloon by `num_bytes` to reclaim guest memory. Called in response to
+    /// memory-trimming notifications.
     pub fn set_memory_balloon(&self, num_bytes: u64) -> Result<(), Error> {
-        let command = BalloonControlCommand::Adjust { num_bytes, wait_for_success: false };
-        if let Err(e) = vm_control::client::handle_request(
-            &VmRequest::BalloonCommand(command),
-            &self.crosvm_control_socket_path,
-        ) {
-            bail!("Error sending balloon adjustment: {:?}", e);
+        let socket_path_cstring = path_to_cstring(&self.crosvm_control_socket_path);
+        // SAFETY: Pointer is valid for the lifetime of the call.
+        let success = unsafe {
+            crosvm_control::crosvm_client_balloon_vms(socket_path_cstring.as_ptr(), num_bytes)
+        };
+        if !success {
+            bail!("Error sending balloon adjustment");
         }
         Ok(())
     }
@@ -704,26 +716,28 @@
         Ok(())
     }
 
-    /// Suspends the VM
+    /// Suspends the VM's vCPUs.
     pub fn suspend(&self) -> Result<(), Error> {
-        match vm_control::client::handle_request(
-            &VmRequest::SuspendVcpus,
-            &self.crosvm_control_socket_path,
-        ) {
-            Ok(VmResponse::Ok) => Ok(()),
-            e => bail!("Failed to suspend VM: {e:?}"),
+        let socket_path_cstring = path_to_cstring(&self.crosvm_control_socket_path);
+        // SAFETY: Pointer is valid for the lifetime of the call.
+        let success =
+            unsafe { crosvm_control::crosvm_client_suspend_vm(socket_path_cstring.as_ptr()) };
+        if !success {
+            bail!("Failed to suspend VM");
         }
+        Ok(())
     }
 
-    /// Resumes the suspended VM
+    /// Resumes the VM's vCPUs.
     pub fn resume(&self) -> Result<(), Error> {
-        match vm_control::client::handle_request(
-            &VmRequest::ResumeVcpus,
-            &self.crosvm_control_socket_path,
-        ) {
-            Ok(VmResponse::Ok) => Ok(()),
-            e => bail!("Failed to resume: {e:?}"),
+        let socket_path_cstring = path_to_cstring(&self.crosvm_control_socket_path);
+        // SAFETY: Pointer is valid for the lifetime of the call.
+        let success =
+            unsafe { crosvm_control::crosvm_client_resume_vm(socket_path_cstring.as_ptr()) };
+        if !success {
+            bail!("Failed to resume VM");
         }
+        Ok(())
     }
 }
 
@@ -884,6 +898,39 @@
     }
 }
 
+fn run_virtiofs(config: &CrosvmConfig) -> io::Result<()> {
+    for shared_path in &config.shared_paths {
+        let ugid_map_value = format!(
+            "{} {} {} {} {} /",
+            shared_path.guest_uid,
+            shared_path.guest_gid,
+            shared_path.host_uid,
+            shared_path.host_gid,
+            shared_path.mask,
+        );
+
+        let cfg_arg = format!("writeback=true,cache_policy=always,ugid_map='{}'", ugid_map_value);
+
+        let mut command = Command::new(CROSVM_PATH);
+        command
+            .arg("device")
+            .arg("fs")
+            .arg(format!("--socket={}", &shared_path.socket_path))
+            .arg(format!("--tag={}", &shared_path.tag))
+            .arg(format!("--shared-dir={}", &shared_path.path))
+            .arg("--cfg")
+            .arg(cfg_arg.as_str())
+            .arg("--disable-sandbox");
+
+        print_crosvm_args(&command);
+
+        let result = SharedChild::spawn(&mut command)?;
+        info!("Spawned virtiofs crosvm({})", result.id());
+    }
+
+    Ok(())
+}
+
 /// Starts an instance of `crosvm` to manage a new VM.
 fn run_vm(
     config: CrosvmConfig,
@@ -893,6 +940,9 @@
     validate_config(&config)?;
 
     let mut command = Command::new(CROSVM_PATH);
+
+    let vm_name = "crosvm_".to_owned() + &config.name;
+    command.arg0(vm_name.clone());
     // TODO(qwandor): Remove --disable-sandbox.
     command
         .arg("--extended-status")
@@ -901,6 +951,8 @@
         .arg("--log-level")
         .arg("info,disk=warn")
         .arg("run")
+        .arg("--name")
+        .arg(vm_name)
         .arg("--disable-sandbox")
         .arg("--cid")
         .arg(config.cid.to_string());
@@ -1062,6 +1114,9 @@
         command.arg(add_preserved_fd(&mut preserved_fds, kernel));
     }
 
+    #[cfg(target_arch = "aarch64")]
+    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));
@@ -1187,6 +1242,12 @@
         command.arg(vfio_argument_for_platform_device(&device)?);
     }
 
+    for shared_path in &config.shared_paths {
+        command
+            .arg("--vhost-user-fs")
+            .arg(format!("{},tag={}", &shared_path.socket_path, &shared_path.tag));
+    }
+
     debug!("Preserving FDs {:?}", preserved_fds);
     command.preserved_fds(preserved_fds);
 
@@ -1296,3 +1357,13 @@
     socket::listen(&fd, socket::Backlog::new(127).unwrap()).context("listen failed")?;
     Ok(fd)
 }
+
+fn path_to_cstring(path: &Path) -> CString {
+    if let Some(s) = path.to_str() {
+        if let Ok(s) = CString::new(s) {
+            return s;
+        }
+    }
+    // The path contains invalid utf8 or a null, which should never happen.
+    panic!("bad path: {path:?}");
+}
diff --git a/android/virtualizationservice/aidl/android/system/virtualizationservice/SharedPath.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/SharedPath.aidl
new file mode 100644
index 0000000..7be7a5f
--- /dev/null
+++ b/android/virtualizationservice/aidl/android/system/virtualizationservice/SharedPath.aidl
@@ -0,0 +1,43 @@
+/*
+ * 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 android.system.virtualizationservice;
+
+/** Shared directory path between host and guest */
+parcelable SharedPath {
+    /** Shared path between host and guest */
+    String sharedPath;
+
+    /** UID of the path on the host */
+    int hostUid;
+
+    /** GID of the path on the host */
+    int hostGid;
+
+    /** UID of the path on the guest */
+    int guestUid;
+
+    /** GID of the path on the guest */
+    int guestGid;
+
+    /** umask settings for the path */
+    int mask;
+
+    /** virtiofs unique tag per path */
+    String tag;
+
+    /** socket name for vhost-user-fs */
+    String socket;
+}
diff --git a/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index f559a71..9f2a23e 100644
--- a/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -21,6 +21,7 @@
 import android.system.virtualizationservice.DisplayConfig;
 import android.system.virtualizationservice.GpuConfig;
 import android.system.virtualizationservice.InputDevice;
+import android.system.virtualizationservice.SharedPath;
 import android.system.virtualizationservice.UsbConfig;
 
 /** Raw configuration for running a VM. */
@@ -52,6 +53,9 @@
     /** Disk images to be made available to the VM. */
     DiskImage[] disks;
 
+    /** Shared paths between host and guest */
+    SharedPath[] sharedPaths;
+
     /** Whether the VM should be a protected VM. */
     boolean protectedVm;
 
diff --git a/build/apex/Android.bp b/build/apex/Android.bp
index f794239..e485aa7 100644
--- a/build/apex/Android.bp
+++ b/build/apex/Android.bp
@@ -285,9 +285,11 @@
         "libz",
     ],
     data: [
-        ":com.android.virt",
         ":test.com.android.virt.pem",
     ],
+    device_common_data: [
+        ":com.android.virt",
+    ],
     test_suites: ["general-tests"],
 }
 
diff --git a/build/microdroid/Android.bp b/build/microdroid/Android.bp
index 27d0246..abb97da 100644
--- a/build/microdroid/Android.bp
+++ b/build/microdroid/Android.bp
@@ -197,7 +197,7 @@
     no_full_install: true,
 }
 
-genrule {
+java_genrule {
     name: "microdroid_build_prop_gen_x86_64",
     srcs: [
         "build.prop",
@@ -215,7 +215,7 @@
         "echo ro.product.cpu.abi=x86_64) > $(out)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_build_prop_gen_arm64",
     srcs: [
         "build.prop",
@@ -597,6 +597,7 @@
 // HACK: use cc_genrule for arch-specific properties
 cc_genrule {
     name: "microdroid_kernel_hashes_rs",
+    compile_multilib: "first",
     srcs: [":microdroid_kernel"],
     arch: {
         arm64: {
@@ -621,6 +622,7 @@
 
 rust_library_rlib {
     name: "libmicrodroid_kernel_hashes",
+    compile_multilib: "first",
     srcs: [":microdroid_kernel_hashes_rs"],
     crate_name: "microdroid_kernel_hashes",
     prefer_rlib: true,
diff --git a/build/microdroid/initrd/Android.bp b/build/microdroid/initrd/Android.bp
index 9904511..6d45417 100644
--- a/build/microdroid/initrd/Android.bp
+++ b/build/microdroid/initrd/Android.bp
@@ -30,7 +30,7 @@
     srcs: ["gen_vbmeta_bootconfig.py"],
 }
 
-genrule {
+java_genrule {
     name: "microdroid_initrd_gen",
     srcs: [
         ":microdroid_ramdisk",
@@ -40,7 +40,7 @@
     cmd: "cat $(in) > $(out)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_gki-android15-6.6_initrd_gen_arm64",
     srcs: [
         ":microdroid_ramdisk",
@@ -51,7 +51,7 @@
     cmd: "cat $(in) > $(out)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_gki-android15-6.6_initrd_gen_x86_64",
     srcs: [
         ":microdroid_ramdisk",
@@ -63,7 +63,7 @@
 }
 
 // This contains vbmeta hashes & related (boot)configs which are passed to kernel/init
-genrule {
+java_genrule {
     name: "microdroid_vbmeta_bootconfig_gen",
     srcs: [":microdroid_vbmeta"],
     out: ["bootconfig_microdroid_vbmeta"],
@@ -84,7 +84,7 @@
     ":microdroid_vbmeta_bootconfig_gen",
 ]
 
-genrule {
+java_genrule {
     name: "microdroid_initrd_debuggable_arm64",
     tools: ["initrd_bootconfig"],
     srcs: [
@@ -95,7 +95,7 @@
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_gki-android15-6.6_initrd_debuggable_arm64",
     tools: ["initrd_bootconfig"],
     srcs: [
@@ -106,7 +106,7 @@
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_initrd_debuggable_x86_64",
     tools: ["initrd_bootconfig"],
     srcs: [
@@ -117,7 +117,7 @@
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_gki-android15-6.6_initrd_debuggable_x86_64",
     tools: ["initrd_bootconfig"],
     srcs: [
@@ -128,7 +128,7 @@
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_initrd_normal_arm64",
     tools: ["initrd_bootconfig"],
     srcs: [
@@ -139,7 +139,7 @@
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_gki-android15-6.6_initrd_normal_arm64",
     tools: ["initrd_bootconfig"],
     srcs: [
@@ -150,7 +150,7 @@
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_initrd_normal_x86_64",
     tools: ["initrd_bootconfig"],
     srcs: [
@@ -161,7 +161,7 @@
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_gki-android15-6.6_initrd_normal_x86_64",
     tools: ["initrd_bootconfig"],
     srcs: [
diff --git a/guest/trusty/security_vm/launcher/Android.bp b/guest/trusty/security_vm/launcher/Android.bp
new file mode 100644
index 0000000..ff628fd
--- /dev/null
+++ b/guest/trusty/security_vm/launcher/Android.bp
@@ -0,0 +1,20 @@
+rust_binary {
+    name: "trusty_security_vm_launcher",
+    crate_name: "trusty_security_vm_launcher",
+    srcs: ["src/main.rs"],
+    edition: "2021",
+    prefer_rlib: true,
+    rustlibs: [
+        "android.system.virtualizationservice-rust",
+        "libanyhow",
+        "libclap",
+        "libvmclient",
+    ],
+    bootstrap: true,
+    apex_available: ["//apex_available:platform"],
+    system_ext_specific: true,
+    enabled: select(release_flag("RELEASE_AVF_ENABLE_EARLY_VM"), {
+        true: true,
+        false: false,
+    }),
+}
diff --git a/guest/trusty/security_vm/launcher/src/main.rs b/guest/trusty/security_vm/launcher/src/main.rs
new file mode 100644
index 0000000..c5cc6b4
--- /dev/null
+++ b/guest/trusty/security_vm/launcher/src/main.rs
@@ -0,0 +1,84 @@
+// 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.
+
+//! A client for trusty security VMs during early boot.
+
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    IVirtualizationService::IVirtualizationService, VirtualMachineConfig::VirtualMachineConfig,
+    VirtualMachineRawConfig::VirtualMachineRawConfig,
+};
+use android_system_virtualizationservice::binder::{ParcelFileDescriptor, Strong};
+use anyhow::{Context, Result};
+use clap::Parser;
+use std::fs::File;
+use std::path::PathBuf;
+use vmclient::VmInstance;
+
+#[derive(Parser)]
+struct Args {
+    /// Path to the trusty kernel image.
+    #[arg(long)]
+    kernel: PathBuf,
+
+    /// Whether the VM is protected or not.
+    #[arg(long)]
+    protected: bool,
+}
+
+fn get_service() -> Result<Strong<dyn IVirtualizationService>> {
+    let virtmgr = vmclient::VirtualizationService::new_early()
+        .context("Failed to spawn VirtualizationService")?;
+    virtmgr.connect().context("Failed to connect to VirtualizationService")
+}
+
+fn main() -> Result<()> {
+    let args = Args::parse();
+
+    let service = get_service()?;
+
+    let kernel =
+        File::open(&args.kernel).with_context(|| format!("Failed to open {:?}", &args.kernel))?;
+
+    let vm_config = VirtualMachineConfig::RawConfig(VirtualMachineRawConfig {
+        name: "trusty_security_vm_launcher".to_owned(),
+        kernel: Some(ParcelFileDescriptor::new(kernel)),
+        protectedVm: args.protected,
+        memoryMib: 128,
+        platformVersion: "~1.0".to_owned(),
+        // TODO: add instanceId
+        ..Default::default()
+    });
+
+    println!("creating VM");
+    let vm = VmInstance::create(
+        service.as_ref(),
+        &vm_config,
+        // console_in, console_out, and log will be redirected to the kernel log by virtmgr
+        None, // console_in
+        None, // console_out
+        None, // log
+        None, // dump_dt
+        None, // callback
+    )
+    .context("Failed to create VM")?;
+    vm.start().context("Failed to start VM")?;
+
+    println!("started trusty_security_vm_launcher VM");
+    let death_reason = vm.wait_for_death();
+    eprintln!("trusty_security_vm_launcher ended: {:?}", death_reason);
+
+    // TODO(b/331320802): we may want to use android logger instead of stdio_to_kmsg?
+
+    Ok(())
+}
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
index 2aa38ce..0b7059a 100644
--- a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -40,6 +40,7 @@
 import android.sysprop.HypervisorProperties;
 import android.system.virtualizationservice.DiskImage;
 import android.system.virtualizationservice.Partition;
+import android.system.virtualizationservice.SharedPath;
 import android.system.virtualizationservice.UsbConfig;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
 import android.system.virtualizationservice.VirtualMachinePayloadConfig;
@@ -707,6 +708,15 @@
             config.disks[i].partitions = partitions.toArray(new Partition[0]);
         }
 
+        config.sharedPaths =
+                new SharedPath
+                        [Optional.ofNullable(customImageConfig.getSharedPaths())
+                                .map(arr -> arr.length)
+                                .orElse(0)];
+        for (int i = 0; i < config.sharedPaths.length; i++) {
+            config.sharedPaths[i] = customImageConfig.getSharedPaths()[i].toParcelable();
+        }
+
         config.displayConfig =
                 Optional.ofNullable(customImageConfig.getDisplayConfig())
                         .map(dc -> dc.toParcelable())
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
index 9774585..9b0709d 100644
--- a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -54,6 +54,7 @@
     @Nullable private final String bootloaderPath;
     @Nullable private final String[] params;
     @Nullable private final Disk[] disks;
+    @Nullable private final SharedPath[] sharedPaths;
     @Nullable private final DisplayConfig displayConfig;
     @Nullable private final AudioConfig audioConfig;
     private final boolean touch;
@@ -96,6 +97,11 @@
         return params;
     }
 
+    @Nullable
+    public SharedPath[] getSharedPaths() {
+        return sharedPaths;
+    }
+
     public boolean useTouch() {
         return touch;
     }
@@ -132,6 +138,7 @@
             String bootloaderPath,
             String[] params,
             Disk[] disks,
+            SharedPath[] sharedPaths,
             DisplayConfig displayConfig,
             boolean touch,
             boolean keyboard,
@@ -149,6 +156,7 @@
         this.bootloaderPath = bootloaderPath;
         this.params = params;
         this.disks = disks;
+        this.sharedPaths = sharedPaths;
         this.displayConfig = displayConfig;
         this.touch = touch;
         this.keyboard = keyboard;
@@ -300,6 +308,91 @@
     }
 
     /** @hide */
+    public static final class SharedPath {
+        private final String path;
+        private final int hostUid;
+        private final int hostGid;
+        private final int guestUid;
+        private final int guestGid;
+        private final int mask;
+        private final String tag;
+        private final String socket;
+
+        public SharedPath(
+                String path,
+                int hostUid,
+                int hostGid,
+                int guestUid,
+                int guestGid,
+                int mask,
+                String tag,
+                String socket) {
+            this.path = path;
+            this.hostUid = hostUid;
+            this.hostGid = hostGid;
+            this.guestUid = guestUid;
+            this.guestGid = guestGid;
+            this.mask = mask;
+            this.tag = tag;
+            this.socket = socket;
+        }
+
+        android.system.virtualizationservice.SharedPath toParcelable() {
+            android.system.virtualizationservice.SharedPath parcelable =
+                    new android.system.virtualizationservice.SharedPath();
+            parcelable.sharedPath = this.path;
+            parcelable.hostUid = this.hostUid;
+            parcelable.hostGid = this.hostGid;
+            parcelable.guestUid = this.guestUid;
+            parcelable.guestGid = this.guestGid;
+            parcelable.mask = this.mask;
+            parcelable.tag = this.tag;
+            parcelable.socket = this.socket;
+            return parcelable;
+        }
+
+        /** @hide */
+        public String getSharedPath() {
+            return path;
+        }
+
+        /** @hide */
+        public int getHostUid() {
+            return hostUid;
+        }
+
+        /** @hide */
+        public int getHostGid() {
+            return hostGid;
+        }
+
+        /** @hide */
+        public int getGuestUid() {
+            return guestUid;
+        }
+
+        /** @hide */
+        public int getGuestGid() {
+            return guestGid;
+        }
+
+        /** @hide */
+        public int getMask() {
+            return mask;
+        }
+
+        /** @hide */
+        public String getTag() {
+            return tag;
+        }
+
+        /** @hide */
+        public String getSocket() {
+            return socket;
+        }
+    }
+
+    /** @hide */
     public static final class Disk {
         private final boolean writable;
         private final String imagePath;
@@ -366,6 +459,7 @@
         private String bootloaderPath;
         private List<String> params = new ArrayList<>();
         private List<Disk> disks = new ArrayList<>();
+        private List<SharedPath> sharedPaths = new ArrayList<>();
         private AudioConfig audioConfig;
         private DisplayConfig displayConfig;
         private boolean touch;
@@ -413,6 +507,12 @@
         }
 
         /** @hide */
+        public Builder addSharedPath(SharedPath path) {
+            this.sharedPaths.add(path);
+            return this;
+        }
+
+        /** @hide */
         public Builder addParam(String param) {
             this.params.add(param);
             return this;
@@ -493,6 +593,7 @@
                     this.bootloaderPath,
                     this.params.toArray(new String[0]),
                     this.disks.toArray(new Disk[0]),
+                    this.sharedPaths.toArray(new SharedPath[0]),
                     displayConfig,
                     touch,
                     keyboard,
diff --git a/libs/libservice_vm_fake_chain/Android.bp b/libs/libservice_vm_fake_chain/Android.bp
index 39f36eb..56fb22a 100644
--- a/libs/libservice_vm_fake_chain/Android.bp
+++ b/libs/libservice_vm_fake_chain/Android.bp
@@ -18,6 +18,7 @@
 
 rust_defaults {
     name: "libservice_vm_fake_chain_defaults",
+    compile_multilib: "first",
     crate_name: "service_vm_fake_chain",
     defaults: ["avf_build_flags_rust"],
     srcs: ["src/lib.rs"],
diff --git a/libs/libservice_vm_requests/Android.bp b/libs/libservice_vm_requests/Android.bp
index 57da012..d87b087 100644
--- a/libs/libservice_vm_requests/Android.bp
+++ b/libs/libservice_vm_requests/Android.bp
@@ -4,6 +4,7 @@
 
 rust_defaults {
     name: "libservice_vm_requests_nostd_defaults",
+    compile_multilib: "first",
     crate_name: "service_vm_requests",
     defaults: ["avf_build_flags_rust"],
     srcs: ["src/lib.rs"],
diff --git a/libs/vbmeta/Android.bp b/libs/vbmeta/Android.bp
index 9a7375d..15c7b4a 100644
--- a/libs/vbmeta/Android.bp
+++ b/libs/vbmeta/Android.bp
@@ -31,7 +31,7 @@
         "libanyhow",
         "libtempfile",
     ],
-    data: [
+    device_common_data: [
         ":avb_testkey_rsa2048",
         ":avb_testkey_rsa4096",
         ":avb_testkey_rsa8192",
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java
index 6d39b46..5d6b13f 100644
--- a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java
@@ -17,6 +17,7 @@
 package com.android.virtualization.vmlauncher;
 
 import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.graphics.Rect;
 import android.system.virtualmachine.VirtualMachineConfig;
 import android.system.virtualmachine.VirtualMachineCustomImageConfig;
@@ -25,6 +26,7 @@
 import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
 import android.system.virtualmachine.VirtualMachineCustomImageConfig.GpuConfig;
 import android.system.virtualmachine.VirtualMachineCustomImageConfig.Partition;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.SharedPath;
 import android.util.DisplayMetrics;
 import android.view.WindowManager;
 import android.view.WindowMetrics;
@@ -34,6 +36,7 @@
 
 import java.io.FileReader;
 import java.util.Arrays;
+import java.util.Objects;
 
 /** This class and its inner classes model vm_config.json. */
 class ConfigJson {
@@ -60,6 +63,7 @@
     private InputJson input;
     private AudioJson audio;
     private DiskJson[] disks;
+    private SharedPathJson[] sharedPath;
     private DisplayJson display;
     private GpuJson gpu;
 
@@ -141,9 +145,36 @@
             Arrays.stream(disks).map(d -> d.toConfig()).forEach(builder::addDisk);
         }
 
+        if (sharedPath != null) {
+            Arrays.stream(sharedPath)
+                    .map(d -> d.toConfig(context))
+                    .filter(Objects::nonNull)
+                    .forEach(builder::addSharedPath);
+        }
         return builder.build();
     }
 
+    private static class SharedPathJson {
+        private SharedPathJson() {}
+
+        // Package ID of Terminal app.
+        private static final String TERMINAL_PACKAGE_ID =
+                "com.google.android.virtualization.terminal";
+        private String sharedPath;
+
+        private SharedPath toConfig(Context context) {
+            try {
+                int uid =
+                        context.getPackageManager()
+                                .getPackageUidAsUser(TERMINAL_PACKAGE_ID, context.getUserId());
+
+                return new SharedPath(sharedPath, uid, uid, 0, 0, 0007, "android", "android");
+            } catch (NameNotFoundException e) {
+                return null;
+            }
+        }
+    }
+
     private static class InputJson {
         private InputJson() {}
 
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java
index eb6dd77..17dc8dd 100644
--- a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java
@@ -38,17 +38,15 @@
 
     private static final String VM_CONFIG_FILENAME = "vm_config.json";
     private static final String COMPRESSED_PAYLOAD_FILENAME = "images.tar.gz";
+    private static final String INSTALLATION_COMPLETED_FILENAME = "completed";
     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();
+        return getInternalStorageDir(context).toPath().resolve(VM_CONFIG_FILENAME).toString();
     }
 
     public static boolean isImageInstalled(Context context) {
-        return Files.exists(Path.of(getVmConfigPath(context)));
+        return Files.exists(getInstallationCompletedPath(context));
     }
 
     private static Path getPayloadPath() {
@@ -65,9 +63,18 @@
         return Files.exists(getPayloadPath());
     }
 
+    public static File getInternalStorageDir(Context context) {
+        return new File(context.getFilesDir(), PAYLOAD_DIR);
+    }
+
+    private static Path getInstallationCompletedPath(Context context) {
+        return getInternalStorageDir(context).toPath().resolve(INSTALLATION_COMPLETED_FILENAME);
+    }
+
     public static boolean installImageFromExternalStorage(Context context) {
         if (!payloadFromExternalStorageExists()) {
             Log.d(TAG, "no artifact file from external storage");
+            return false;
         }
         Path payloadPath = getPayloadPath();
         try (BufferedInputStream inputStream =
@@ -89,10 +96,6 @@
             Log.e(TAG, "installation failed", e);
             return false;
         }
-        if (!isImageInstalled(context)) {
-            return false;
-        }
-
         if (!resolvePathInVmConfig(context)) {
             Log.d(TAG, "resolving path failed");
             try {
@@ -110,6 +113,14 @@
             Log.d(TAG, "failed to remove installed payload", e);
         }
 
+        // Create marker for installation done.
+        try {
+            File file = new File(getInstallationCompletedPath(context).toString());
+            file.createNewFile();
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to mark install completed", e);
+            return false;
+        }
         return true;
     }
 
@@ -124,7 +135,7 @@
         };
     }
 
-    private static boolean resolvePathInVmConfig(Context context) {
+    public static boolean resolvePathInVmConfig(Context context) {
         try {
             String replacedVmConfig =
                     String.join(
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 849cc24..3731854 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
@@ -62,8 +62,8 @@
 
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
-        if (isVmRunning()) {
-            Log.d(TAG, "there is already the running VM instance");
+        if (mVirtualMachine != null) {
+            Log.d(TAG, "VM instance is already started");
             return START_NOT_STICKY;
         }
         mExecutorService = Executors.newCachedThreadPool();
@@ -114,12 +114,14 @@
     @Override
     public void onDestroy() {
         super.onDestroy();
-        if (isVmRunning()) {
-            try {
-                mVirtualMachine.stop();
-                stopForeground(STOP_FOREGROUND_REMOVE);
-            } catch (VirtualMachineException e) {
-                Log.e(TAG, "failed to stop a VM instance", e);
+        if (mVirtualMachine != null) {
+            if (mVirtualMachine.getStatus() == VirtualMachine.STATUS_RUNNING) {
+                try {
+                    mVirtualMachine.stop();
+                    stopForeground(STOP_FOREGROUND_REMOVE);
+                } catch (VirtualMachineException e) {
+                    Log.e(TAG, "failed to stop a VM instance", e);
+                }
             }
             mExecutorService.shutdownNow();
             mExecutorService = null;
@@ -128,11 +130,6 @@
         stopDebianServer();
     }
 
-    private boolean isVmRunning() {
-        return mVirtualMachine != null
-                && mVirtualMachine.getStatus() == VirtualMachine.STATUS_RUNNING;
-    }
-
     private void startDebianServer() {
         new Thread(
                         () -> {
diff --git a/tests/authfs/benchmarks/Android.bp b/tests/authfs/benchmarks/Android.bp
index 27a6af1..aad8d59 100644
--- a/tests/authfs/benchmarks/Android.bp
+++ b/tests/authfs/benchmarks/Android.bp
@@ -19,7 +19,7 @@
         "open_then_run",
     ],
     per_testcase_directory: true,
-    data: [
+    device_common_data: [
         ":authfs_test_files",
         ":MicrodroidTestApp",
     ],
@@ -43,7 +43,7 @@
 java_genrule {
     name: "measure_io_as_jar",
     out: ["measure_io.jar"],
-    srcs: [
+    device_first_srcs: [
         ":measure_io",
     ],
     tools: ["soong_zip"],
diff --git a/tests/authfs/hosttests/Android.bp b/tests/authfs/hosttests/Android.bp
index 83ef853..50dbc05 100644
--- a/tests/authfs/hosttests/Android.bp
+++ b/tests/authfs/hosttests/Android.bp
@@ -20,6 +20,8 @@
     per_testcase_directory: true,
     data: [
         ":authfs_test_files",
+    ],
+    device_common_data: [
         ":MicrodroidTestApp",
     ],
 }
diff --git a/tests/benchmark_hostside/Android.bp b/tests/benchmark_hostside/Android.bp
index b613a8a..e91ac8f 100644
--- a/tests/benchmark_hostside/Android.bp
+++ b/tests/benchmark_hostside/Android.bp
@@ -18,7 +18,7 @@
     test_suites: [
         "general-tests",
     ],
-    data: [
+    device_common_data: [
         ":MicrodroidTestApp",
     ],
 }
diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp
index d0838a6..0f2fe58 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -20,7 +20,7 @@
         "microdroid_payload_metadata",
     ],
     per_testcase_directory: true,
-    data: [
+    device_common_data: [
         ":MicrodroidTestApp",
         ":MicrodroidTestAppUpdated",
         ":microdroid_general_sepolicy.conf",
diff --git a/tests/pvmfw/Android.bp b/tests/pvmfw/Android.bp
index 0483066..e124e55 100644
--- a/tests/pvmfw/Android.bp
+++ b/tests/pvmfw/Android.bp
@@ -45,13 +45,17 @@
     ],
     per_testcase_directory: true,
     data: [
+        "assets/bcc.dat",
+    ],
+    device_common_data: [
         ":MicrodroidTestApp",
-        ":pvmfw_test",
         ":test_avf_debug_policy_with_ramdump",
         ":test_avf_debug_policy_without_ramdump",
         ":test_avf_debug_policy_with_adb",
         ":test_avf_debug_policy_without_adb",
-        "assets/bcc.dat",
+    ],
+    device_first_data: [
+        ":pvmfw_test",
     ],
     data_device_bins_first: ["dtc_static"],
 }
diff --git a/tests/vendor_images/Android.bp b/tests/vendor_images/Android.bp
index 66f0219..0430eaa 100644
--- a/tests/vendor_images/Android.bp
+++ b/tests/vendor_images/Android.bp
@@ -2,19 +2,13 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-prebuilt_etc {
-    name: "vendor_sign_key",
-    src: ":avb_testkey_rsa4096",
-    installable: false,
-}
-
 android_filesystem {
     name: "test_microdroid_vendor_image",
     partition_name: "microdroid-vendor",
     type: "ext4",
     file_contexts: ":microdroid_vendor_file_contexts.gen",
     use_avb: true,
-    avb_private_key: ":vendor_sign_key",
+    avb_private_key: ":avb_testkey_rsa4096",
     rollback_index: 5,
 }
 
@@ -24,7 +18,7 @@
     type: "ext4",
     file_contexts: ":microdroid_vendor_file_contexts.gen",
     use_avb: true,
-    avb_private_key: ":vendor_sign_key",
+    avb_private_key: ":avb_testkey_rsa4096",
 }
 
 android_filesystem {