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)
+ }
+ }
+}