VmLauncherService to kotlin

Bug: 383243644
Test: run terminal
Change-Id: I85176ed8aae9e81741dc757f1562b67e110f324a
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt
index e84250b..31c9a91 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt
@@ -142,7 +142,7 @@
         private const val HOST_URL = "https://dl.google.com/android/ferrochrome/$BUILD_TAG"
 
         @JvmStatic
-        fun getSdcardPathForTesting(): Path? {
+        fun getSdcardPathForTesting(): Path {
             return Environment.getExternalStoragePublicDirectory(DIR_IN_SDCARD).toPath()
         }
 
@@ -151,7 +151,7 @@
          */
         @JvmStatic
         fun fromSdCard(): ImageArchive {
-            return ImageArchive(getSdcardPathForTesting()!!.resolve(ARCHIVE_NAME))
+            return ImageArchive(getSdcardPathForTesting().resolve(ARCHIVE_NAME))
         }
 
         /**
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Runner.kt b/android/TerminalApp/java/com/android/virtualization/terminal/Runner.kt
index 86dadbe..897e182 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/Runner.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Runner.kt
@@ -32,7 +32,7 @@
     val exitStatus = callback.finishedSuccessfully
 
     private class Callback : VirtualMachineCallback {
-        val finishedSuccessfully: CompletableFuture<Boolean?> = CompletableFuture<Boolean?>()
+        val finishedSuccessfully: CompletableFuture<Boolean> = CompletableFuture<Boolean>()
 
         override fun onPayloadStarted(vm: VirtualMachine) {
             // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
deleted file mode 100644
index 09b58d3..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
+++ /dev/null
@@ -1,375 +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.terminal;
-
-import static com.android.virtualization.terminal.MainActivity.TAG;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.drawable.Icon;
-import android.net.nsd.NsdManager;
-import android.net.nsd.NsdServiceInfo;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Parcel;
-import android.os.ResultReceiver;
-import android.system.virtualmachine.VirtualMachine;
-import android.system.virtualmachine.VirtualMachineConfig;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig.Disk;
-import android.system.virtualmachine.VirtualMachineException;
-import android.util.Log;
-import android.widget.Toast;
-
-import io.grpc.Grpc;
-import io.grpc.InsecureServerCredentials;
-import io.grpc.Metadata;
-import io.grpc.Server;
-import io.grpc.ServerCall;
-import io.grpc.ServerCallHandler;
-import io.grpc.ServerInterceptor;
-import io.grpc.Status;
-import io.grpc.okhttp.OkHttpServerBuilder;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Objects;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-public class VmLauncherService extends Service {
-    private static final String EXTRA_NOTIFICATION = "EXTRA_NOTIFICATION";
-    private static final String ACTION_START_VM_LAUNCHER_SERVICE =
-            "android.virtualization.START_VM_LAUNCHER_SERVICE";
-
-    public static final String ACTION_STOP_VM_LAUNCHER_SERVICE =
-            "android.virtualization.STOP_VM_LAUNCHER_SERVICE";
-
-    private static final int RESULT_START = 0;
-    private static final int RESULT_STOP = 1;
-    private static final int RESULT_ERROR = 2;
-
-    private ExecutorService mExecutorService;
-    private VirtualMachine mVirtualMachine;
-    private ResultReceiver mResultReceiver;
-    private Server mServer;
-    private DebianServiceImpl mDebianService;
-    private PortNotifier mPortNotifier;
-
-    private static Intent getMyIntent(Context context) {
-        return new Intent(context.getApplicationContext(), VmLauncherService.class);
-    }
-
-    public interface VmLauncherServiceCallback {
-        void onVmStart();
-
-        void onVmStop();
-
-        void onVmError();
-    }
-
-    public static void run(
-            Context context, VmLauncherServiceCallback callback, Notification notification) {
-        Intent i = getMyIntent(context);
-        if (i == null) {
-            return;
-        }
-        ResultReceiver resultReceiver =
-                new ResultReceiver(new Handler(Looper.myLooper())) {
-                    @Override
-                    protected void onReceiveResult(int resultCode, Bundle resultData) {
-                        if (callback == null) {
-                            return;
-                        }
-                        switch (resultCode) {
-                            case RESULT_START:
-                                callback.onVmStart();
-                                return;
-                            case RESULT_STOP:
-                                callback.onVmStop();
-                                return;
-                            case RESULT_ERROR:
-                                callback.onVmError();
-                                return;
-                        }
-                    }
-                };
-        i.putExtra(Intent.EXTRA_RESULT_RECEIVER, getResultReceiverForIntent(resultReceiver));
-        i.putExtra(VmLauncherService.EXTRA_NOTIFICATION, notification);
-        context.startForegroundService(i);
-    }
-
-    private static ResultReceiver getResultReceiverForIntent(ResultReceiver r) {
-        Parcel parcel = Parcel.obtain();
-        r.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        r = ResultReceiver.CREATOR.createFromParcel(parcel);
-        parcel.recycle();
-        return r;
-    }
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        return null;
-    }
-
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        if (Objects.equals(intent.getAction(), ACTION_STOP_VM_LAUNCHER_SERVICE)) {
-
-            if (mDebianService != null && mDebianService.shutdownDebian()) {
-                // During shutdown, change the notification content to indicate that it's closing
-                Notification notification = createNotificationForTerminalClose();
-                getSystemService(NotificationManager.class).notify(this.hashCode(), notification);
-            } else {
-                // If there is no Debian service or it fails to shutdown, just stop the service.
-                stopSelf();
-            }
-            return START_NOT_STICKY;
-        }
-        if (mVirtualMachine != null) {
-            Log.d(TAG, "VM instance is already started");
-            return START_NOT_STICKY;
-        }
-        mExecutorService =
-                Executors.newCachedThreadPool(new TerminalThreadFactory(getApplicationContext()));
-
-        InstalledImage image = InstalledImage.getDefault(this);
-        ConfigJson json = ConfigJson.from(this, image.getConfigPath());
-        VirtualMachineConfig.Builder configBuilder = json.toConfigBuilder(this);
-        VirtualMachineCustomImageConfig.Builder customImageConfigBuilder =
-                json.toCustomImageConfigBuilder(this);
-        if (overrideConfigIfNecessary(customImageConfigBuilder)) {
-            configBuilder.setCustomImageConfig(customImageConfigBuilder.build());
-        }
-        VirtualMachineConfig config = configBuilder.build();
-
-        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) {
-            throw new RuntimeException("cannot create runner", e);
-        }
-        mVirtualMachine = runner.getVm();
-        mResultReceiver =
-                intent.getParcelableExtra(Intent.EXTRA_RESULT_RECEIVER, ResultReceiver.class);
-
-        runner.getExitStatus()
-                .thenAcceptAsync(
-                        success -> {
-                            if (mResultReceiver != null) {
-                                mResultReceiver.send(success ? RESULT_STOP : RESULT_ERROR, null);
-                            }
-                            stopSelf();
-                        });
-        Path logPath = getFileStreamPath(mVirtualMachine.getName() + ".log").toPath();
-        Logger.setup(mVirtualMachine, logPath, mExecutorService);
-
-        Notification notification =
-                intent.getParcelableExtra(EXTRA_NOTIFICATION, Notification.class);
-
-        startForeground(this.hashCode(), notification);
-
-        mResultReceiver.send(RESULT_START, null);
-
-        mPortNotifier = new PortNotifier(this);
-
-        // TODO: dedup this part
-        NsdManager nsdManager = getSystemService(NsdManager.class);
-        NsdServiceInfo info = new NsdServiceInfo();
-        info.setServiceType("_http._tcp");
-        info.setServiceName("ttyd");
-        nsdManager.registerServiceInfoCallback(
-                info,
-                mExecutorService,
-                new NsdManager.ServiceInfoCallback() {
-                    @Override
-                    public void onServiceInfoCallbackRegistrationFailed(int errorCode) {}
-
-                    @Override
-                    public void onServiceInfoCallbackUnregistered() {}
-
-                    @Override
-                    public void onServiceLost() {}
-
-                    @Override
-                    public void onServiceUpdated(NsdServiceInfo info) {
-                        nsdManager.unregisterServiceInfoCallback(this);
-                        Log.i(TAG, "Service found: " + info.toString());
-                        String ipAddress = info.getHostAddresses().get(0).getHostAddress();
-                        startDebianServer(ipAddress);
-                    }
-                });
-
-        return START_NOT_STICKY;
-    }
-
-    private Notification createNotificationForTerminalClose() {
-        Intent stopIntent = new Intent();
-        stopIntent.setClass(this, VmLauncherService.class);
-        stopIntent.setAction(VmLauncherService.ACTION_STOP_VM_LAUNCHER_SERVICE);
-        PendingIntent stopPendingIntent =
-                PendingIntent.getService(
-                        this,
-                        0,
-                        stopIntent,
-                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
-        Icon icon = Icon.createWithResource(getResources(), R.drawable.ic_launcher_foreground);
-        String stopActionText =
-                getResources().getString(R.string.service_notification_force_quit_action);
-        String stopNotificationTitle =
-                getResources().getString(R.string.service_notification_close_title);
-        return new Notification.Builder(this, this.getPackageName())
-                .setSmallIcon(R.drawable.ic_launcher_foreground)
-                .setContentTitle(stopNotificationTitle)
-                .setOngoing(true)
-                .setSilent(true)
-                .addAction(
-                        new Notification.Action.Builder(icon, stopActionText, stopPendingIntent)
-                                .build())
-                .build();
-    }
-
-    private boolean overrideConfigIfNecessary(VirtualMachineCustomImageConfig.Builder builder) {
-        boolean changed = false;
-        // TODO: check if ANGLE is enabled for the app.
-        if (Files.exists(ImageArchive.getSdcardPathForTesting().resolve("virglrenderer"))) {
-            builder.setGpuConfig(
-                    new VirtualMachineCustomImageConfig.GpuConfig.Builder()
-                            .setBackend("virglrenderer")
-                            .setRendererUseEgl(true)
-                            .setRendererUseGles(true)
-                            .setRendererUseGlx(false)
-                            .setRendererUseSurfaceless(true)
-                            .setRendererUseVulkan(false)
-                            .setContextTypes(new String[] {"virgl2"})
-                            .build());
-            Toast.makeText(this, R.string.virgl_enabled, Toast.LENGTH_SHORT).show();
-            changed = true;
-        }
-
-        InstalledImage image = InstalledImage.getDefault(this);
-        if (image.hasBackup()) {
-            Path backup = image.getBackupFile();
-            builder.addDisk(Disk.RWDisk(backup.toString()));
-            changed = true;
-        }
-        return changed;
-    }
-
-    private void startDebianServer(String ipAddress) {
-        ServerInterceptor interceptor =
-                new ServerInterceptor() {
-                    @Override
-                    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
-                            ServerCall<ReqT, RespT> call,
-                            Metadata headers,
-                            ServerCallHandler<ReqT, RespT> next) {
-                        InetSocketAddress remoteAddr =
-                                (InetSocketAddress)
-                                        call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR);
-
-                        if (remoteAddr != null
-                                && Objects.equals(
-                                        remoteAddr.getAddress().getHostAddress(), ipAddress)) {
-                            // Allow the request only if it is from VM
-                            return next.startCall(call, headers);
-                        }
-                        Log.d(TAG, "blocked grpc request from " + remoteAddr);
-                        call.close(Status.Code.PERMISSION_DENIED.toStatus(), new Metadata());
-                        return new ServerCall.Listener<ReqT>() {};
-                    }
-                };
-        try {
-            // TODO(b/372666638): gRPC for java doesn't support vsock for now.
-            int port = 0;
-            mDebianService = new DebianServiceImpl(this);
-            mServer =
-                    OkHttpServerBuilder.forPort(port, InsecureServerCredentials.create())
-                            .intercept(interceptor)
-                            .addService(mDebianService)
-                            .build()
-                            .start();
-        } catch (IOException e) {
-            Log.d(TAG, "grpc server error", e);
-            return;
-        }
-
-        mExecutorService.execute(
-                () -> {
-                    // TODO(b/373533555): we can use mDNS for that.
-                    String debianServicePortFileName = "debian_service_port";
-                    File debianServicePortFile = new File(getFilesDir(), debianServicePortFileName);
-                    try (FileOutputStream writer = new FileOutputStream(debianServicePortFile)) {
-                        writer.write(String.valueOf(mServer.getPort()).getBytes());
-                    } catch (IOException e) {
-                        Log.d(TAG, "cannot write grpc port number", e);
-                    }
-                });
-    }
-
-    public static void stop(Context context) {
-        Intent i = getMyIntent(context);
-        i.setAction(VmLauncherService.ACTION_STOP_VM_LAUNCHER_SERVICE);
-        context.startService(i);
-    }
-
-    @Override
-    public void onDestroy() {
-        if (mPortNotifier != null) {
-            mPortNotifier.stop();
-        }
-        getSystemService(NotificationManager.class).cancelAll();
-        stopDebianServer();
-        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;
-            mVirtualMachine = null;
-        }
-        super.onDestroy();
-    }
-
-    private void stopDebianServer() {
-        if (mDebianService != null) {
-            mDebianService.killForwarderHost();
-        }
-        if (mServer != null) {
-            mServer.shutdown();
-        }
-    }
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
new file mode 100644
index 0000000..2796b86
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -0,0 +1,355 @@
+/*
+ * 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.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Icon
+import android.net.nsd.NsdManager
+import android.net.nsd.NsdServiceInfo
+import android.os.Bundle
+import android.os.Handler
+import android.os.IBinder
+import android.os.Looper
+import android.os.Parcel
+import android.os.ResultReceiver
+import android.os.Trace
+import android.system.virtualmachine.VirtualMachine
+import android.system.virtualmachine.VirtualMachineCustomImageConfig
+import android.system.virtualmachine.VirtualMachineException
+import android.util.Log
+import android.widget.Toast
+import com.android.virtualization.terminal.MainActivity.TAG
+import com.android.virtualization.terminal.Runner.Companion.create
+import com.android.virtualization.terminal.VmLauncherService.VmLauncherServiceCallback
+import io.grpc.Grpc
+import io.grpc.InsecureServerCredentials
+import io.grpc.Metadata
+import io.grpc.Server
+import io.grpc.ServerCall
+import io.grpc.ServerCallHandler
+import io.grpc.ServerInterceptor
+import io.grpc.Status
+import io.grpc.okhttp.OkHttpServerBuilder
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.lang.RuntimeException
+import java.net.InetSocketAddress
+import java.net.SocketAddress
+import java.nio.file.Files
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class VmLauncherService : Service() {
+    // TODO: using lateinit for some fields to avoid null
+    private var mExecutorService: ExecutorService? = null
+    private var mVirtualMachine: VirtualMachine? = null
+    private var mResultReceiver: ResultReceiver? = null
+    private var mServer: Server? = null
+    private var mDebianService: DebianServiceImpl? = null
+    private var mPortNotifier: PortNotifier? = null
+
+    interface VmLauncherServiceCallback {
+        fun onVmStart()
+
+        fun onVmStop()
+
+        fun onVmError()
+    }
+
+    override fun onBind(intent: Intent?): IBinder? {
+        return null
+    }
+
+    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+        if (intent.action == ACTION_STOP_VM_LAUNCHER_SERVICE) {
+            if (mDebianService != null && mDebianService!!.shutdownDebian()) {
+                // During shutdown, change the notification content to indicate that it's closing
+                val notification = createNotificationForTerminalClose()
+                getSystemService<NotificationManager?>(NotificationManager::class.java)
+                    .notify(this.hashCode(), notification)
+            } else {
+                // If there is no Debian service or it fails to shutdown, just stop the service.
+                stopSelf()
+            }
+            return START_NOT_STICKY
+        }
+        if (mVirtualMachine != null) {
+            Log.d(TAG, "VM instance is already started")
+            return START_NOT_STICKY
+        }
+        mExecutorService = Executors.newCachedThreadPool(TerminalThreadFactory(applicationContext))
+
+        val image = InstalledImage.getDefault(this)
+        val json = ConfigJson.from(this, image.configPath)
+        val configBuilder = json.toConfigBuilder(this)
+        val customImageConfigBuilder = json.toCustomImageConfigBuilder(this)
+        if (overrideConfigIfNecessary(customImageConfigBuilder)) {
+            configBuilder.setCustomImageConfig(customImageConfigBuilder.build())
+        }
+        val config = configBuilder.build()
+
+        Trace.beginSection("vmCreate")
+        val runner: Runner =
+            try {
+                create(this, config)
+            } catch (e: VirtualMachineException) {
+                throw RuntimeException("cannot create runner", e)
+            }
+        Trace.endSection()
+        Trace.beginAsyncSection("debianBoot", 0)
+
+        mVirtualMachine = runner.vm
+        mResultReceiver =
+            intent.getParcelableExtra<ResultReceiver?>(
+                Intent.EXTRA_RESULT_RECEIVER,
+                ResultReceiver::class.java,
+            )
+
+        runner.exitStatus.thenAcceptAsync { success: Boolean ->
+            mResultReceiver?.send(if (success) RESULT_STOP else RESULT_ERROR, null)
+            stopSelf()
+        }
+        val logPath = getFileStreamPath(mVirtualMachine!!.name + ".log").toPath()
+        Logger.setup(mVirtualMachine!!, logPath, mExecutorService!!)
+
+        val notification =
+            intent.getParcelableExtra<Notification?>(EXTRA_NOTIFICATION, Notification::class.java)
+
+        startForeground(this.hashCode(), notification)
+
+        mResultReceiver!!.send(RESULT_START, null)
+
+        mPortNotifier = PortNotifier(this)
+
+        // TODO: dedup this part
+        val nsdManager = getSystemService<NsdManager?>(NsdManager::class.java)
+        val info = NsdServiceInfo()
+        info.serviceType = "_http._tcp"
+        info.serviceName = "ttyd"
+        nsdManager.registerServiceInfoCallback(
+            info,
+            mExecutorService!!,
+            object : NsdManager.ServiceInfoCallback {
+                override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {}
+
+                override fun onServiceInfoCallbackUnregistered() {}
+
+                override fun onServiceLost() {}
+
+                override fun onServiceUpdated(info: NsdServiceInfo) {
+                    nsdManager.unregisterServiceInfoCallback(this)
+                    Log.i(TAG, "Service found: $info")
+                    startDebianServer(info.hostAddresses[0].hostAddress)
+                }
+            },
+        )
+
+        return START_NOT_STICKY
+    }
+
+    private fun createNotificationForTerminalClose(): Notification {
+        val stopIntent = Intent()
+        stopIntent.setClass(this, VmLauncherService::class.java)
+        stopIntent.setAction(ACTION_STOP_VM_LAUNCHER_SERVICE)
+        val stopPendingIntent =
+            PendingIntent.getService(
+                this,
+                0,
+                stopIntent,
+                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
+            )
+        val icon = Icon.createWithResource(resources, R.drawable.ic_launcher_foreground)
+        val stopActionText: String? =
+            resources.getString(R.string.service_notification_force_quit_action)
+        val stopNotificationTitle: String? =
+            resources.getString(R.string.service_notification_close_title)
+        return Notification.Builder(this, this.packageName)
+            .setSmallIcon(R.drawable.ic_launcher_foreground)
+            .setContentTitle(stopNotificationTitle)
+            .setOngoing(true)
+            .setSilent(true)
+            .addAction(Notification.Action.Builder(icon, stopActionText, stopPendingIntent).build())
+            .build()
+    }
+
+    private fun overrideConfigIfNecessary(
+        builder: VirtualMachineCustomImageConfig.Builder
+    ): Boolean {
+        var changed = false
+        // TODO: check if ANGLE is enabled for the app.
+        if (Files.exists(ImageArchive.getSdcardPathForTesting().resolve("virglrenderer"))) {
+            builder.setGpuConfig(
+                VirtualMachineCustomImageConfig.GpuConfig.Builder()
+                    .setBackend("virglrenderer")
+                    .setRendererUseEgl(true)
+                    .setRendererUseGles(true)
+                    .setRendererUseGlx(false)
+                    .setRendererUseSurfaceless(true)
+                    .setRendererUseVulkan(false)
+                    .setContextTypes(arrayOf<String>("virgl2"))
+                    .build()
+            )
+            Toast.makeText(this, R.string.virgl_enabled, Toast.LENGTH_SHORT).show()
+            changed = true
+        }
+
+        val image = InstalledImage.getDefault(this)
+        if (image.hasBackup()) {
+            val backup = image.backupFile
+            builder.addDisk(VirtualMachineCustomImageConfig.Disk.RWDisk(backup.toString()))
+            changed = true
+        }
+        return changed
+    }
+
+    private fun startDebianServer(ipAddress: String?) {
+        val interceptor: ServerInterceptor =
+            object : ServerInterceptor {
+                override fun <ReqT, RespT> interceptCall(
+                    call: ServerCall<ReqT?, RespT?>,
+                    headers: Metadata?,
+                    next: ServerCallHandler<ReqT?, RespT?>,
+                ): ServerCall.Listener<ReqT?>? {
+                    val remoteAddr =
+                        call.attributes.get<SocketAddress?>(Grpc.TRANSPORT_ATTR_REMOTE_ADDR)
+                            as InetSocketAddress?
+
+                    if (remoteAddr?.address?.hostAddress == ipAddress) {
+                        // Allow the request only if it is from VM
+                        return next.startCall(call, headers)
+                    }
+                    Log.d(TAG, "blocked grpc request from $remoteAddr")
+                    call.close(Status.Code.PERMISSION_DENIED.toStatus(), Metadata())
+                    return object : ServerCall.Listener<ReqT?>() {}
+                }
+            }
+        try {
+            // TODO(b/372666638): gRPC for java doesn't support vsock for now.
+            val port = 0
+            mDebianService = DebianServiceImpl(this)
+            mServer =
+                OkHttpServerBuilder.forPort(port, InsecureServerCredentials.create())
+                    .intercept(interceptor)
+                    .addService(mDebianService)
+                    .build()
+                    .start()
+        } catch (e: IOException) {
+            Log.d(TAG, "grpc server error", e)
+            return
+        }
+
+        mExecutorService!!.execute(
+            Runnable {
+                // TODO(b/373533555): we can use mDNS for that.
+                val debianServicePortFile = File(filesDir, "debian_service_port")
+                try {
+                    FileOutputStream(debianServicePortFile).use { writer ->
+                        writer.write(mServer!!.port.toString().toByteArray())
+                    }
+                } catch (e: IOException) {
+                    Log.d(TAG, "cannot write grpc port number", e)
+                }
+            }
+        )
+    }
+
+    override fun onDestroy() {
+        mPortNotifier?.stop()
+        getSystemService<NotificationManager?>(NotificationManager::class.java).cancelAll()
+        stopDebianServer()
+        if (mVirtualMachine != null) {
+            if (mVirtualMachine!!.getStatus() == VirtualMachine.STATUS_RUNNING) {
+                try {
+                    mVirtualMachine!!.stop()
+                    stopForeground(STOP_FOREGROUND_REMOVE)
+                } catch (e: VirtualMachineException) {
+                    Log.e(TAG, "failed to stop a VM instance", e)
+                }
+            }
+            mExecutorService?.shutdownNow()
+            mExecutorService = null
+            mVirtualMachine = null
+        }
+        super.onDestroy()
+    }
+
+    private fun stopDebianServer() {
+        mDebianService?.killForwarderHost()
+        mServer?.shutdown()
+    }
+
+    companion object {
+        private const val EXTRA_NOTIFICATION = "EXTRA_NOTIFICATION"
+        private const val ACTION_START_VM_LAUNCHER_SERVICE =
+            "android.virtualization.START_VM_LAUNCHER_SERVICE"
+
+        const val ACTION_STOP_VM_LAUNCHER_SERVICE: String =
+            "android.virtualization.STOP_VM_LAUNCHER_SERVICE"
+
+        private const val RESULT_START = 0
+        private const val RESULT_STOP = 1
+        private const val RESULT_ERROR = 2
+
+        private fun getMyIntent(context: Context): Intent {
+            return Intent(context.getApplicationContext(), VmLauncherService::class.java)
+        }
+
+        @JvmStatic
+        fun run(
+            context: Context,
+            callback: VmLauncherServiceCallback?,
+            notification: Notification?,
+        ) {
+            val i = getMyIntent(context)
+            val resultReceiver: ResultReceiver =
+                object : ResultReceiver(Handler(Looper.myLooper()!!)) {
+                    override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
+                        if (callback == null) {
+                            return
+                        }
+                        when (resultCode) {
+                            RESULT_START -> callback.onVmStart()
+                            RESULT_STOP -> callback.onVmStop()
+                            RESULT_ERROR -> callback.onVmError()
+                        }
+                    }
+                }
+            i.putExtra(Intent.EXTRA_RESULT_RECEIVER, getResultReceiverForIntent(resultReceiver))
+            i.putExtra(EXTRA_NOTIFICATION, notification)
+            context.startForegroundService(i)
+        }
+
+        private fun getResultReceiverForIntent(r: ResultReceiver): ResultReceiver {
+            val parcel = Parcel.obtain()
+            r.writeToParcel(parcel, 0)
+            parcel.setDataPosition(0)
+            return ResultReceiver.CREATOR.createFromParcel(parcel).also { parcel.recycle() }
+        }
+
+        @JvmStatic
+        fun stop(context: Context) {
+            val i = getMyIntent(context)
+            i.setAction(ACTION_STOP_VM_LAUNCHER_SERVICE)
+            context.startService(i)
+        }
+    }
+}