Unite vm_launcher_lib into VmTerminalApp
Bug: 379800648
Test: build and run terminal app
Change-Id: I9ad7d68ec8adc0cb787a70169ea70f32e4bdd3b2
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index 3b5f9b8..2711af0 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -11,15 +11,22 @@
resource_dirs: ["res"],
asset_dirs: ["assets"],
static_libs: [
- "VmTerminalApp.aidl-java",
- "vm_launcher_lib",
"androidx-constraintlayout_constraintlayout",
- "com.google.android.material_material",
"androidx.window_window",
+ "apache-commons-compress",
+ "com.google.android.material_material",
+ "debian-service-grpclib-lite",
+ "gson",
+ "VmTerminalApp.aidl-java",
],
jni_libs: [
"libforwarder_host_jni",
],
+ libs: [
+ "androidx.annotation_annotation",
+ "framework-virtualization.impl",
+ "framework-annotations-lib",
+ ],
use_embedded_native_libs: true,
platform_apis: true,
privileged: true,
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index b74b8b0..dad07ee 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -76,7 +76,7 @@
android:stopWithTask="true" />
<service
- android:name="com.android.virtualization.vmlauncher.VmLauncherService"
+ android:name=".VmLauncherService"
android:exported="false"
android:foregroundServiceType="specialUse"
android:stopWithTask="true" >
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java b/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java
new file mode 100644
index 0000000..e1342e9
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.virtualization.terminal;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.Rect;
+import android.os.Environment;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.AudioConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.Disk;
+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;
+
+import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
+
+import java.io.FileReader;
+import java.util.Arrays;
+import java.util.Objects;
+
+/** This class and its inner classes model vm_config.json. */
+class ConfigJson {
+ private static final boolean DEBUG = true;
+
+ private ConfigJson() {}
+
+ @SerializedName("protected")
+ private boolean isProtected;
+
+ private String name;
+ private String cpu_topology;
+ private String platform_version;
+ private int memory_mib = 1024;
+ private String console_input_device;
+ private String bootloader;
+ private String kernel;
+ private String initrd;
+ private String params;
+ private boolean debuggable;
+ private boolean console_out;
+ private boolean connect_console;
+ private boolean network;
+ private InputJson input;
+ private AudioJson audio;
+ private DiskJson[] disks;
+ private SharedPathJson[] sharedPath;
+ private DisplayJson display;
+ private GpuJson gpu;
+
+ /** Parses JSON file at jsonPath */
+ static ConfigJson from(String jsonPath) {
+ try (FileReader r = new FileReader(jsonPath)) {
+ return new Gson().fromJson(r, ConfigJson.class);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to parse " + jsonPath, e);
+ }
+ }
+
+ private int getCpuTopology() {
+ switch (cpu_topology) {
+ case "one_cpu":
+ return VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU;
+ case "match_host":
+ return VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
+ default:
+ throw new RuntimeException("invalid cpu topology: " + cpu_topology);
+ }
+ }
+
+ private int getDebugLevel() {
+ return debuggable
+ ? VirtualMachineConfig.DEBUG_LEVEL_FULL
+ : VirtualMachineConfig.DEBUG_LEVEL_NONE;
+ }
+
+ /** Converts this parsed JSON into VirtualMachieConfig Builder */
+ VirtualMachineConfig.Builder toConfigBuilder(Context context) {
+ return new VirtualMachineConfig.Builder(context)
+ .setProtectedVm(isProtected)
+ .setMemoryBytes((long) memory_mib * 1024 * 1024)
+ .setConsoleInputDevice(console_input_device)
+ .setCpuTopology(getCpuTopology())
+ .setCustomImageConfig(toCustomImageConfigBuilder(context).build())
+ .setDebugLevel(getDebugLevel())
+ .setVmOutputCaptured(console_out)
+ .setConnectVmConsole(connect_console);
+ }
+
+ VirtualMachineCustomImageConfig.Builder toCustomImageConfigBuilder(Context context) {
+ VirtualMachineCustomImageConfig.Builder builder =
+ new VirtualMachineCustomImageConfig.Builder();
+
+ builder.setName(name)
+ .setBootloaderPath(bootloader)
+ .setKernelPath(kernel)
+ .setInitrdPath(initrd)
+ .useNetwork(network);
+
+ if (input != null) {
+ builder.useTouch(input.touchscreen)
+ .useKeyboard(input.keyboard)
+ .useMouse(input.mouse)
+ .useTrackpad(input.trackpad)
+ .useSwitches(input.switches);
+ }
+
+ if (audio != null) {
+ builder.setAudioConfig(audio.toConfig());
+ }
+
+ if (display != null) {
+ builder.setDisplayConfig(display.toConfig(context));
+ }
+
+ if (gpu != null) {
+ builder.setGpuConfig(gpu.toConfig());
+ }
+
+ if (params != null) {
+ Arrays.stream(params.split(" ")).forEach(builder::addParam);
+ }
+
+ if (disks != null) {
+ 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;
+ }
+
+ private static class SharedPathJson {
+ private SharedPathJson() {}
+
+ private String sharedPath;
+ private static final int GUEST_UID = 1000;
+ private static final int GUEST_GID = 100;
+
+ private SharedPath toConfig(Context context) {
+ try {
+ int terminalUid = getTerminalUid(context);
+ if (sharedPath.contains("emulated")) {
+ if (Environment.isExternalStorageManager()) {
+ int currentUserId = context.getUserId();
+ String path = sharedPath + "/" + currentUserId + "/Download";
+ return new SharedPath(
+ path,
+ terminalUid,
+ terminalUid,
+ GUEST_UID,
+ GUEST_GID,
+ 0007,
+ "android",
+ "android");
+ }
+ return null;
+ }
+ return new SharedPath(
+ sharedPath, terminalUid, terminalUid, 0, 0, 0007, "internal", "internal");
+ } catch (NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ private int getTerminalUid(Context context) throws NameNotFoundException {
+ return context.getPackageManager()
+ .getPackageUidAsUser(context.getPackageName(), context.getUserId());
+ }
+ }
+
+ private static class InputJson {
+ private InputJson() {}
+
+ private boolean touchscreen;
+ private boolean keyboard;
+ private boolean mouse;
+ private boolean switches;
+ private boolean trackpad;
+ }
+
+ private static class AudioJson {
+ private AudioJson() {}
+
+ private boolean microphone;
+ private boolean speaker;
+
+ private AudioConfig toConfig() {
+ return new AudioConfig.Builder()
+ .setUseMicrophone(microphone)
+ .setUseSpeaker(speaker)
+ .build();
+ }
+ }
+
+ private static class DiskJson {
+ private DiskJson() {}
+
+ private boolean writable;
+ private String image;
+ private PartitionJson[] partitions;
+
+ private Disk toConfig() {
+ Disk d = writable ? Disk.RWDisk(image) : Disk.RODisk(image);
+ for (PartitionJson pj : partitions) {
+ boolean writable = this.writable && pj.writable;
+ d.addPartition(new Partition(pj.label, pj.path, writable, pj.guid));
+ }
+ return d;
+ }
+ }
+
+ private static class PartitionJson {
+ private PartitionJson() {}
+
+ private boolean writable;
+ private String label;
+ private String path;
+ private String guid;
+ }
+
+ private static class DisplayJson {
+ private DisplayJson() {}
+
+ private float scale;
+ private int refresh_rate;
+ private int width_pixels;
+ private int height_pixels;
+
+ private DisplayConfig toConfig(Context context) {
+ WindowManager wm = context.getSystemService(WindowManager.class);
+ WindowMetrics metrics = wm.getCurrentWindowMetrics();
+ Rect dispBounds = metrics.getBounds();
+
+ int width = width_pixels > 0 ? width_pixels : dispBounds.right;
+ int height = height_pixels > 0 ? height_pixels : dispBounds.bottom;
+
+ int dpi = (int) (DisplayMetrics.DENSITY_DEFAULT * metrics.getDensity());
+ if (scale > 0.0f) {
+ dpi = (int) (dpi * scale);
+ }
+
+ int refreshRate = (int) context.getDisplay().getRefreshRate();
+ if (this.refresh_rate != 0) {
+ refreshRate = this.refresh_rate;
+ }
+
+ return new DisplayConfig.Builder()
+ .setWidth(width)
+ .setHeight(height)
+ .setHorizontalDpi(dpi)
+ .setVerticalDpi(dpi)
+ .setRefreshRate(refreshRate)
+ .build();
+ }
+ }
+
+ private static class GpuJson {
+ private GpuJson() {}
+
+ private String backend;
+ private String pci_address;
+ private String renderer_features;
+ private boolean renderer_use_egl = true;
+ private boolean renderer_use_gles = true;
+ private boolean renderer_use_glx = false;
+ private boolean renderer_use_surfaceless = true;
+ private boolean renderer_use_vulkan = false;
+ private String[] context_types;
+
+ private GpuConfig toConfig() {
+ return new GpuConfig.Builder()
+ .setBackend(backend)
+ .setPciAddress(pci_address)
+ .setRendererFeatures(renderer_features)
+ .setRendererUseEgl(renderer_use_egl)
+ .setRendererUseGles(renderer_use_gles)
+ .setRendererUseGlx(renderer_use_glx)
+ .setRendererUseSurfaceless(renderer_use_surfaceless)
+ .setRendererUseVulkan(renderer_use_vulkan)
+ .setContextTypes(context_types)
+ .build();
+ }
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
new file mode 100644
index 0000000..0b65cf6
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.virtualization.terminal;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import androidx.annotation.Keep;
+
+import com.android.virtualization.terminal.proto.DebianServiceGrpc;
+import com.android.virtualization.terminal.proto.ForwardingRequestItem;
+import com.android.virtualization.terminal.proto.IpAddr;
+import com.android.virtualization.terminal.proto.QueueOpeningRequest;
+import com.android.virtualization.terminal.proto.ReportVmActivePortsRequest;
+import com.android.virtualization.terminal.proto.ReportVmActivePortsResponse;
+import com.android.virtualization.terminal.proto.ReportVmIpAddrResponse;
+
+import io.grpc.stub.StreamObserver;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+final class DebianServiceImpl extends DebianServiceGrpc.DebianServiceImplBase {
+ public static final String TAG = "DebianService";
+ private static final String PREFERENCE_FILE_KEY =
+ "com.android.virtualization.terminal.PREFERENCE_FILE_KEY";
+ private static final String PREFERENCE_FORWARDING_PORTS = "PREFERENCE_FORWARDING_PORTS";
+ private static final String PREFERENCE_FORWARDING_PORT_IS_ENABLED_PREFIX =
+ "PREFERENCE_FORWARDING_PORT_IS_ENABLED_";
+
+ private final Context mContext;
+ private final SharedPreferences mSharedPref;
+ private SharedPreferences.OnSharedPreferenceChangeListener mPortForwardingListener;
+ private final DebianServiceCallback mCallback;
+
+ static {
+ System.loadLibrary("forwarder_host_jni");
+ }
+
+ DebianServiceImpl(Context context, DebianServiceCallback callback) {
+ super();
+ mCallback = callback;
+ mContext = context;
+ mSharedPref = mContext.getSharedPreferences(PREFERENCE_FILE_KEY, Context.MODE_PRIVATE);
+ }
+
+ @Override
+ public void reportVmActivePorts(
+ ReportVmActivePortsRequest request,
+ StreamObserver<ReportVmActivePortsResponse> responseObserver) {
+ Log.d(DebianServiceImpl.TAG, "reportVmActivePorts: " + request.toString());
+
+ SharedPreferences.Editor editor = mSharedPref.edit();
+ Set<String> ports = new HashSet<>();
+ for (int port : request.getPortsList()) {
+ ports.add(Integer.toString(port));
+ if (!mSharedPref.contains(
+ PREFERENCE_FORWARDING_PORT_IS_ENABLED_PREFIX + Integer.toString(port))) {
+ editor.putBoolean(
+ PREFERENCE_FORWARDING_PORT_IS_ENABLED_PREFIX + Integer.toString(port),
+ false);
+ }
+ }
+ editor.putStringSet(PREFERENCE_FORWARDING_PORTS, ports);
+ editor.apply();
+
+ ReportVmActivePortsResponse reply =
+ ReportVmActivePortsResponse.newBuilder().setSuccess(true).build();
+ responseObserver.onNext(reply);
+ responseObserver.onCompleted();
+ }
+
+ @Override
+ public void reportVmIpAddr(
+ IpAddr request, StreamObserver<ReportVmIpAddrResponse> responseObserver) {
+ Log.d(DebianServiceImpl.TAG, "reportVmIpAddr: " + request.toString());
+ mCallback.onIpAddressAvailable(request.getAddr());
+ ReportVmIpAddrResponse reply = ReportVmIpAddrResponse.newBuilder().setSuccess(true).build();
+ responseObserver.onNext(reply);
+ responseObserver.onCompleted();
+ }
+
+ @Override
+ public void openForwardingRequestQueue(
+ QueueOpeningRequest request, StreamObserver<ForwardingRequestItem> responseObserver) {
+ Log.d(DebianServiceImpl.TAG, "OpenForwardingRequestQueue");
+ mPortForwardingListener =
+ new SharedPreferences.OnSharedPreferenceChangeListener() {
+ @Override
+ public void onSharedPreferenceChanged(
+ SharedPreferences sharedPreferences, String key) {
+ if (key.startsWith(PREFERENCE_FORWARDING_PORT_IS_ENABLED_PREFIX)
+ || key.equals(PREFERENCE_FORWARDING_PORTS)) {
+ updateListeningPorts();
+ }
+ }
+ };
+ mSharedPref.registerOnSharedPreferenceChangeListener(mPortForwardingListener);
+ updateListeningPorts();
+ runForwarderHost(request.getCid(), new ForwarderHostCallback(responseObserver));
+ responseObserver.onCompleted();
+ }
+
+ @Keep
+ private static class ForwarderHostCallback {
+ private StreamObserver<ForwardingRequestItem> mResponseObserver;
+
+ ForwarderHostCallback(StreamObserver<ForwardingRequestItem> responseObserver) {
+ mResponseObserver = responseObserver;
+ }
+
+ private void onForwardingRequestReceived(int guestTcpPort, int vsockPort) {
+ ForwardingRequestItem item =
+ ForwardingRequestItem.newBuilder()
+ .setGuestTcpPort(guestTcpPort)
+ .setVsockPort(vsockPort)
+ .build();
+ mResponseObserver.onNext(item);
+ }
+ }
+
+ private static native void runForwarderHost(int cid, ForwarderHostCallback callback);
+
+ private static native void terminateForwarderHost();
+
+ void killForwarderHost() {
+ Log.d(DebianServiceImpl.TAG, "Stopping port forwarding");
+ if (mPortForwardingListener != null) {
+ mSharedPref.unregisterOnSharedPreferenceChangeListener(mPortForwardingListener);
+ terminateForwarderHost();
+ }
+ }
+
+ private static native void updateListeningPorts(int[] ports);
+
+ private void updateListeningPorts() {
+ updateListeningPorts(
+ mSharedPref
+ .getStringSet(PREFERENCE_FORWARDING_PORTS, Collections.emptySet())
+ .stream()
+ .filter(
+ port ->
+ mSharedPref.getBoolean(
+ PREFERENCE_FORWARDING_PORT_IS_ENABLED_PREFIX + port,
+ false))
+ .map(Integer::valueOf)
+ .mapToInt(Integer::intValue)
+ .toArray());
+ }
+
+ protected interface DebianServiceCallback {
+ void onIpAddressAvailable(String ipAddr);
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallUtils.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallUtils.java
new file mode 100644
index 0000000..b17e636
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallUtils.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.virtualization.terminal;
+
+import android.content.Context;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+public class InstallUtils {
+ private static final String TAG = InstallUtils.class.getSimpleName();
+
+ private static final String VM_CONFIG_FILENAME = "vm_config.json";
+ private static final String COMPRESSED_PAYLOAD_FILENAME = "images.tar.gz";
+ private static final String ROOTFS_FILENAME = "root_part";
+ private static final String BACKUP_FILENAME = "root_part_backup";
+ private static final String INSTALLATION_COMPLETED_FILENAME = "completed";
+ private static final String PAYLOAD_DIR = "linux";
+
+ public static String getVmConfigPath(Context context) {
+ return getInternalStorageDir(context).toPath().resolve(VM_CONFIG_FILENAME).toString();
+ }
+
+ public static boolean isImageInstalled(Context context) {
+ return Files.exists(getInstallationCompletedPath(context));
+ }
+
+ public static void backupRootFs(Context context) throws IOException {
+ Files.move(
+ getRootfsFile(context).toPath(),
+ getBackupFile(context).toPath(),
+ StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ public static boolean createInstalledMarker(Context context) {
+ try {
+ File file = new File(getInstallationCompletedPath(context).toString());
+ return file.createNewFile();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to mark install completed", e);
+ return false;
+ }
+ }
+
+ @VisibleForTesting
+ public static void deleteInstallation(Context context) {
+ FileUtils.deleteContentsAndDir(getInternalStorageDir(context));
+ }
+
+ private static Path getPayloadPath() {
+ File payloadDir = Environment.getExternalStoragePublicDirectory(PAYLOAD_DIR);
+ if (payloadDir == null) {
+ Log.d(TAG, "no payload dir: " + payloadDir);
+ return null;
+ }
+ Path payloadPath = payloadDir.toPath().resolve(COMPRESSED_PAYLOAD_FILENAME);
+ return payloadPath;
+ }
+
+ public static boolean payloadFromExternalStorageExists() {
+ return Files.exists(getPayloadPath());
+ }
+
+ public static File getInternalStorageDir(Context context) {
+ return new File(context.getFilesDir(), PAYLOAD_DIR);
+ }
+
+ public static File getBackupFile(Context context) {
+ return new File(context.getFilesDir(), BACKUP_FILENAME);
+ }
+
+ 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 =
+ new BufferedInputStream(Files.newInputStream(payloadPath));
+ TarArchiveInputStream tar =
+ new TarArchiveInputStream(new GzipCompressorInputStream(inputStream))) {
+ ArchiveEntry entry;
+ Path baseDir = new File(context.getFilesDir(), PAYLOAD_DIR).toPath();
+ Files.createDirectories(baseDir);
+ while ((entry = tar.getNextEntry()) != null) {
+ Path extractTo = baseDir.resolve(entry.getName());
+ if (entry.isDirectory()) {
+ Files.createDirectories(extractTo);
+ } else {
+ Files.copy(tar, extractTo, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "installation failed", e);
+ return false;
+ }
+ if (!resolvePathInVmConfig(context)) {
+ Log.d(TAG, "resolving path failed");
+ try {
+ Files.deleteIfExists(Path.of(getVmConfigPath(context)));
+ } catch (IOException e) {
+ return false;
+ }
+ return false;
+ }
+
+ // remove payload if installation is done.
+ try {
+ Files.deleteIfExists(payloadPath);
+ } catch (IOException e) {
+ Log.d(TAG, "failed to remove installed payload", e);
+ }
+
+ // Create marker for installation done.
+ return createInstalledMarker(context);
+ }
+
+ private static Function<String, String> getReplacer(Context context) {
+ Map<String, String> rules = new HashMap<>();
+ rules.put("\\$PAYLOAD_DIR", new File(context.getFilesDir(), PAYLOAD_DIR).toString());
+ rules.put("\\$USER_ID", String.valueOf(context.getUserId()));
+ rules.put("\\$PACKAGE_NAME", context.getPackageName());
+ String appDataDir = context.getDataDir().toString();
+ // TODO: remove this hack
+ if (context.getUserId() == 0) {
+ appDataDir = "/data/data/" + context.getPackageName();
+ }
+ rules.put("\\$APP_DATA_DIR", appDataDir);
+ return (s) -> {
+ for (Map.Entry<String, String> rule : rules.entrySet()) {
+ s = s.replaceAll(rule.getKey(), rule.getValue());
+ }
+ return s;
+ };
+ }
+
+ public static boolean resolvePathInVmConfig(Context context) {
+ try {
+ String replacedVmConfig =
+ String.join(
+ "\n",
+ Files.readAllLines(Path.of(getVmConfigPath(context))).stream()
+ .map(getReplacer(context))
+ .toList());
+ Files.write(Path.of(getVmConfigPath(context)), replacedVmConfig.getBytes());
+ return true;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ public static File getRootfsFile(Context context) throws FileNotFoundException {
+ File file = new File(getInternalStorageDir(context), ROOTFS_FILENAME);
+ if (!file.exists()) {
+ Log.d(TAG, file.getAbsolutePath() + " - file not found");
+ throw new FileNotFoundException("File not found: " + ROOTFS_FILENAME);
+ }
+ return file;
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
index 83c6b4c..a1d4cb6 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
@@ -35,7 +35,6 @@
import android.widget.TextView;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.virtualization.vmlauncher.InstallUtils;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.android.material.snackbar.Snackbar;
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
index f97f16f..f839c64 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
@@ -29,7 +29,6 @@
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;
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Logger.java b/android/TerminalApp/java/com/android/virtualization/terminal/Logger.java
new file mode 100644
index 0000000..2c0149e
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Logger.java
@@ -0,0 +1,87 @@
+/*
+ * 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.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.util.Log;
+
+import libcore.io.Streams;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Forwards VM's console output to a file on the Android side, and VM's log output to Android logd.
+ */
+class Logger {
+ private Logger() {}
+
+ static void setup(VirtualMachine vm, Path path, ExecutorService executor) {
+ if (vm.getConfig().getDebugLevel() != VirtualMachineConfig.DEBUG_LEVEL_FULL) {
+ return;
+ }
+
+ try {
+ InputStream console = vm.getConsoleOutput();
+ OutputStream file = Files.newOutputStream(path, StandardOpenOption.CREATE);
+ executor.submit(() -> Streams.copy(console, new LineBufferedOutputStream(file)));
+
+ InputStream log = vm.getLogOutput();
+ executor.submit(() -> writeToLogd(log, vm.getName()));
+ } catch (VirtualMachineException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static boolean writeToLogd(InputStream input, String vmName) throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(input));
+ String line;
+ while ((line = reader.readLine()) != null && !Thread.interrupted()) {
+ Log.d(vmName, line);
+ }
+ // TODO: find out why javac complains when the return type of this method is void. It
+ // (incorrectly?) thinks that IOException should be caught inside the lambda.
+ return true;
+ }
+
+ private static class LineBufferedOutputStream extends BufferedOutputStream {
+ LineBufferedOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ @Override
+ public void write(byte[] buf, int off, int len) throws IOException {
+ super.write(buf, off, len);
+ for (int i = 0; i < len; ++i) {
+ if (buf[off + i] == '\n') {
+ flush();
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index 8d03a72..eb0e7e2 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -54,9 +54,6 @@
import androidx.activity.result.contract.ActivityResultContracts;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.virtualization.vmlauncher.InstallUtils;
-import com.android.virtualization.vmlauncher.VmLauncherService;
-import com.android.virtualization.vmlauncher.VmLauncherServices;
import com.google.android.material.appbar.MaterialToolbar;
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Runner.java b/android/TerminalApp/java/com/android/virtualization/terminal/Runner.java
new file mode 100644
index 0000000..a2247b1
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Runner.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.virtualization.terminal;
+
+import android.content.Context;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineCallback;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.system.virtualmachine.VirtualMachineManager;
+import android.util.Log;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ForkJoinPool;
+
+/** Utility class for creating a VM and waiting for it to finish. */
+class Runner {
+ private static final String TAG = Runner.class.getSimpleName();
+ private final VirtualMachine mVirtualMachine;
+ private final Callback mCallback;
+
+ private Runner(VirtualMachine vm, Callback cb) {
+ mVirtualMachine = vm;
+ mCallback = cb;
+ }
+
+ /** Create a virtual machine of the given config, under the given context. */
+ static Runner create(Context context, VirtualMachineConfig config)
+ throws VirtualMachineException {
+ // context may already be the app context, but calling this again is not harmful.
+ // See b/359439878 on why vmm should be obtained from the app context.
+ Context appContext = context.getApplicationContext();
+ VirtualMachineManager vmm = appContext.getSystemService(VirtualMachineManager.class);
+ VirtualMachineCustomImageConfig customConfig = config.getCustomImageConfig();
+ if (customConfig == null) {
+ throw new RuntimeException("CustomImageConfig is missing");
+ }
+
+ String name = customConfig.getName();
+ if (name == null || name.isEmpty()) {
+ throw new RuntimeException("Virtual machine's name is missing in the config");
+ }
+
+ VirtualMachine vm = vmm.getOrCreate(name, config);
+ try {
+ vm.setConfig(config);
+ } catch (VirtualMachineException e) {
+ vmm.delete(name);
+ vm = vmm.create(name, config);
+ Log.w(TAG, "Re-creating virtual machine (" + name + ")", e);
+ }
+
+ Callback cb = new Callback();
+ vm.setCallback(ForkJoinPool.commonPool(), cb);
+ vm.run();
+ return new Runner(vm, cb);
+ }
+
+ /** Give access to the underlying VirtualMachine object. */
+ VirtualMachine getVm() {
+ return mVirtualMachine;
+ }
+
+ /** Get future about VM's exit status. */
+ CompletableFuture<Boolean> getExitStatus() {
+ return mCallback.mFinishedSuccessfully;
+ }
+
+ private static class Callback implements VirtualMachineCallback {
+ final CompletableFuture<Boolean> mFinishedSuccessfully = new CompletableFuture<>();
+
+ @Override
+ public void onPayloadStarted(VirtualMachine vm) {
+ // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
+ }
+
+ @Override
+ public void onPayloadReady(VirtualMachine vm) {
+ // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
+ }
+
+ @Override
+ public void onPayloadFinished(VirtualMachine vm, int exitCode) {
+ // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
+ }
+
+ @Override
+ public void onError(VirtualMachine vm, int errorCode, String message) {
+ Log.e(TAG, "Error from VM. code: " + errorCode + " (" + message + ")");
+ mFinishedSuccessfully.complete(false);
+ }
+
+ @Override
+ public void onStopped(VirtualMachine vm, int reason) {
+ Log.d(TAG, "VM stopped. Reason: " + reason);
+ mFinishedSuccessfully.complete(true);
+ }
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
index 7ccce9c..817808f 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
@@ -27,7 +27,6 @@
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
-import com.android.virtualization.vmlauncher.InstallUtils
import com.google.android.material.button.MaterialButton
import com.google.android.material.slider.Slider
import java.util.regex.Pattern
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
index e2bb28f..ef76e03 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
@@ -22,7 +22,6 @@
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
-import com.android.virtualization.vmlauncher.InstallUtils
import com.google.android.material.card.MaterialCardView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
new file mode 100644
index 0000000..25afcb7
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
@@ -0,0 +1,227 @@
+/*
+ * 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.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+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 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.Path;
+import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class VmLauncherService extends Service implements DebianServiceImpl.DebianServiceCallback {
+ public static final String EXTRA_NOTIFICATION = "EXTRA_NOTIFICATION";
+ private static final String TAG = "VmLauncherService";
+
+ private static final int RESULT_START = 0;
+ private static final int RESULT_STOP = 1;
+ private static final int RESULT_ERROR = 2;
+ private static final int RESULT_IPADDR = 3;
+ private static final String KEY_VM_IP_ADDR = "ip_addr";
+
+ private ExecutorService mExecutorService;
+ private VirtualMachine mVirtualMachine;
+ private ResultReceiver mResultReceiver;
+ private Server mServer;
+ private DebianServiceImpl mDebianService;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private void startForeground(Notification notification) {
+ startForeground(this.hashCode(), notification);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (Objects.equals(
+ intent.getAction(), VmLauncherServices.ACTION_STOP_VM_LAUNCHER_SERVICE)) {
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+ if (mVirtualMachine != null) {
+ Log.d(TAG, "VM instance is already started");
+ return START_NOT_STICKY;
+ }
+ mExecutorService = Executors.newCachedThreadPool();
+
+ ConfigJson json = ConfigJson.from(InstallUtils.getVmConfigPath(this));
+ VirtualMachineConfig.Builder configBuilder = json.toConfigBuilder(this);
+ VirtualMachineCustomImageConfig.Builder customImageConfigBuilder =
+ json.toCustomImageConfigBuilder(this);
+ File backupFile = InstallUtils.getBackupFile(this);
+ if (backupFile.exists()) {
+ customImageConfigBuilder.addDisk(Disk.RWDisk(backupFile.getPath()));
+ 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) {
+ Log.e(TAG, "cannot create runner", e);
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+ 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(notification);
+
+ mResultReceiver.send(RESULT_START, null);
+
+ startDebianServer();
+
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ 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;
+ }
+ }
+
+ private void startDebianServer() {
+ ServerInterceptor interceptor =
+ new ServerInterceptor() {
+ @Override
+ public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
+ ServerCall<ReqT, RespT> call,
+ Metadata headers,
+ ServerCallHandler<ReqT, RespT> next) {
+ // Refer to VirtualizationSystemService.TetheringService
+ final String VM_STATIC_IP_ADDR = "192.168.0.2";
+ InetSocketAddress remoteAddr =
+ (InetSocketAddress)
+ call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR);
+
+ if (remoteAddr != null
+ && Objects.equals(
+ remoteAddr.getAddress().getHostAddress(),
+ VM_STATIC_IP_ADDR)) {
+ // 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, 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);
+ }
+ });
+ }
+
+ private void stopDebianServer() {
+ if (mDebianService != null) {
+ mDebianService.killForwarderHost();
+ }
+ if (mServer != null) {
+ mServer.shutdown();
+ }
+ }
+
+ @Override
+ public void onIpAddressAvailable(String ipAddr) {
+ android.os.Trace.endAsyncSection("debianBoot", 0);
+ Bundle b = new Bundle();
+ b.putString(VmLauncherService.KEY_VM_IP_ADDR, ipAddr);
+ mResultReceiver.send(VmLauncherService.RESULT_IPADDR, b);
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherServices.java b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherServices.java
new file mode 100644
index 0000000..d6c6786
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherServices.java
@@ -0,0 +1,122 @@
+/*
+ * 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.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import java.util.List;
+
+public class VmLauncherServices {
+ private static final String TAG = "VmLauncherServices";
+
+ 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 static final int RESULT_IPADDR = 3;
+ private static final String KEY_VM_IP_ADDR = "ip_addr";
+
+ private static Intent buildVmLauncherServiceIntent(Context context) {
+ Intent i = new Intent();
+ i.setAction(ACTION_START_VM_LAUNCHER_SERVICE);
+
+ Intent intent = new Intent(ACTION_START_VM_LAUNCHER_SERVICE);
+ PackageManager pm = context.getPackageManager();
+ List<ResolveInfo> resolveInfos =
+ pm.queryIntentServices(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (resolveInfos == null || resolveInfos.size() != 1) {
+ Log.e(TAG, "cannot find a service to handle ACTION_START_VM_LAUNCHER_SERVICE");
+ return null;
+ }
+ String packageName = resolveInfos.get(0).serviceInfo.packageName;
+
+ i.setPackage(packageName);
+ return i;
+ }
+
+ public static void stopVmLauncherService(Context context) {
+ Intent i = buildVmLauncherServiceIntent(context);
+ context.stopService(i);
+ }
+
+ public static void startVmLauncherService(
+ Context context, VmLauncherServiceCallback callback, Notification notification) {
+ Intent i = buildVmLauncherServiceIntent(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;
+ case RESULT_IPADDR:
+ callback.onIpAddrAvailable(resultData.getString(KEY_VM_IP_ADDR));
+ return;
+ }
+ }
+ };
+ i.putExtra(Intent.EXTRA_RESULT_RECEIVER, getResultReceiverForIntent(resultReceiver));
+ i.putExtra(VmLauncherService.EXTRA_NOTIFICATION, notification);
+ context.startForegroundService(i);
+ }
+
+ public interface VmLauncherServiceCallback {
+ void onVmStart();
+
+ void onVmStop();
+
+ void onVmError();
+
+ void onIpAddrAvailable(String ipAddr);
+ }
+
+ 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;
+ }
+}
diff --git a/android/TerminalApp/proguard.flags b/android/TerminalApp/proguard.flags
index 8433e82..88b8a9c 100644
--- a/android/TerminalApp/proguard.flags
+++ b/android/TerminalApp/proguard.flags
@@ -11,8 +11,8 @@
#-keep class com.google.gson.stream.** { *; }
# Application classes that will be serialized/deserialized over Gson
--keep class com.android.virtualization.vmlauncher.ConfigJson { <fields>; }
--keep class com.android.virtualization.vmlauncher.ConfigJson$* { <fields>; }
+-keep class com.android.virtualization.terminal.ConfigJson { <fields>; }
+-keep class com.android.virtualization.terminal.ConfigJson$* { <fields>; }
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)