Snap for 12715656 from d1903851e8d86d5b7fddb49892a23c60fdba2ab0 to 25Q1-release
Change-Id: I4b0dda1aa3b7187da21faf9d8303f6f137d8cd21
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index 2711af0..733a72b 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -9,7 +9,6 @@
"java/**/*.kt",
],
resource_dirs: ["res"],
- asset_dirs: ["assets"],
static_libs: [
"androidx-constraintlayout_constraintlayout",
"androidx.window_window",
diff --git a/android/TerminalApp/assets/.gitkeep b/android/TerminalApp/assets/.gitkeep
deleted file mode 100644
index e69de29..0000000
--- a/android/TerminalApp/assets/.gitkeep
+++ /dev/null
diff --git a/android/TerminalApp/assets/client.p12 b/android/TerminalApp/assets/client.p12
deleted file mode 100644
index f1f5820..0000000
--- a/android/TerminalApp/assets/client.p12
+++ /dev/null
Binary files differ
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java b/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java
index e1342e9..ab03049 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java
@@ -35,9 +35,16 @@
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
+import java.io.BufferedReader;
import java.io.FileReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.file.Path;
import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Objects;
+import java.util.stream.Collectors;
/** This class and its inner classes model vm_config.json. */
class ConfigJson {
@@ -69,14 +76,40 @@
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);
+ static ConfigJson from(Context context, Path jsonPath) {
+ try (FileReader fileReader = new FileReader(jsonPath.toFile())) {
+ String content = replaceKeywords(fileReader, context);
+ return new Gson().fromJson(content, ConfigJson.class);
} catch (Exception e) {
throw new RuntimeException("Failed to parse " + jsonPath, e);
}
}
+ private static String replaceKeywords(Reader r, Context context) throws IOException {
+ Map<String, String> rules = new HashMap<>();
+ rules.put("\\$PAYLOAD_DIR", InstalledImage.getDefault(context).getInstallDir().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);
+
+ try (BufferedReader br = new BufferedReader(r)) {
+ return br.lines()
+ .map(
+ line -> {
+ for (Map.Entry<String, String> rule : rules.entrySet()) {
+ line = line.replaceAll(rule.getKey(), rule.getValue());
+ }
+ return line;
+ })
+ .collect(Collectors.joining("\n"));
+ }
+ }
+
private int getCpuTopology() {
switch (cpu_topology) {
case "one_cpu":
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ErrorActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/ErrorActivity.java
index ee1f1ad..44dcce5 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ErrorActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ErrorActivity.java
@@ -16,6 +16,7 @@
package com.android.virtualization.terminal;
+import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
@@ -25,7 +26,14 @@
import androidx.annotation.Nullable;
public class ErrorActivity extends BaseActivity {
- public static final String EXTRA_CAUSE = "cause";
+ private static final String EXTRA_CAUSE = "cause";
+
+ public static void start(Context context, Exception e) {
+ Intent intent = new Intent(context, ErrorActivity.class);
+ intent.putExtra(EXTRA_CAUSE, e);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
new file mode 100644
index 0000000..b2a2085
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.virtualization.terminal;
+
+import android.os.Build;
+import android.os.Environment;
+
+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.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Arrays;
+import java.util.function.Function;
+
+/**
+ * ImageArchive models the archive file (images.tar.gz) where VM payload files are in. This class
+ * provides methods for handling the archive file, most importantly installing it.
+ */
+class ImageArchive {
+ private static final String DIR_IN_SDCARD = "linux";
+ private static final String ARCHIVE_NAME = "images.tar.gz";
+ private static final String HOST_URL = "https://dl.google.com/android/ferrochrome/latest";
+
+ // Only one can be non-null
+ private final URL mUrl;
+ private final Path mPath;
+
+ private ImageArchive(URL url) {
+ mUrl = url;
+ mPath = null;
+ }
+
+ private ImageArchive(Path path) {
+ mUrl = null;
+ mPath = path;
+ }
+
+ public static Path getSdcardPathForTesting() {
+ return Environment.getExternalStoragePublicDirectory(DIR_IN_SDCARD).toPath();
+ }
+
+ /** Creates ImageArchive which is located in the sdcard. This archive is for testing only. */
+ public static ImageArchive fromSdCard() {
+ Path file = getSdcardPathForTesting().resolve(ARCHIVE_NAME);
+ return new ImageArchive(file);
+ }
+
+ /** Creates ImageArchive which is hosted in the Google server. This is the official archive. */
+ public static ImageArchive fromInternet() {
+ String arch = Arrays.asList(Build.SUPPORTED_ABIS).contains("x86_64") ? "x86_64" : "aarch64";
+ try {
+ URL url = new URL(HOST_URL + "/" + arch + "/" + ARCHIVE_NAME);
+ return new ImageArchive(url);
+ } catch (MalformedURLException e) {
+ // cannot happen
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Creates ImageArchive from either SdCard or Internet. SdCard is used only when the build is
+ * debuggable and the file actually exists.
+ */
+ public static ImageArchive getDefault() {
+ ImageArchive archive = fromSdCard();
+ if (Build.isDebuggable() && archive.exists()) {
+ return archive;
+ } else {
+ return fromInternet();
+ }
+ }
+
+ /** Tests if ImageArchive exists on the medium. */
+ public boolean exists() {
+ if (mPath != null) {
+ return Files.exists(mPath);
+ } else {
+ // TODO
+ return true;
+ }
+ }
+
+ /** Returns size of the archive in bytes */
+ public long getSize() throws IOException {
+ if (!exists()) {
+ throw new IllegalStateException("Cannot get size of non existing archive");
+ }
+ if (mPath != null) {
+ return Files.size(mPath);
+ } else {
+ HttpURLConnection conn = null;
+ try {
+ conn = (HttpURLConnection) mUrl.openConnection();
+ conn.setRequestMethod("HEAD");
+ conn.getInputStream();
+ return conn.getContentLength();
+ } finally {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+ }
+ }
+
+ private InputStream getInputStream(Function<InputStream, InputStream> filter)
+ throws IOException {
+ InputStream is = mPath != null ? new FileInputStream(mPath.toFile()) : mUrl.openStream();
+ BufferedInputStream bufStream = new BufferedInputStream(is);
+ return filter == null ? bufStream : filter.apply(bufStream);
+ }
+
+ /**
+ * Installs this ImageArchive to a directory pointed by path. filter can be supplied to provide
+ * an additional input stream which will be used during the installation.
+ */
+ public void installTo(Path dir, Function<InputStream, InputStream> filter) throws IOException {
+ try (InputStream stream = getInputStream(filter);
+ GzipCompressorInputStream gzStream = new GzipCompressorInputStream(stream);
+ TarArchiveInputStream tarStream = new TarArchiveInputStream(gzStream)) {
+
+ Files.createDirectories(dir);
+ ArchiveEntry entry;
+ while ((entry = tarStream.getNextEntry()) != null) {
+ Path to = dir.resolve(entry.getName());
+ if (Files.isDirectory(to)) {
+ Files.createDirectories(to);
+ } else {
+ Files.copy(tarStream, to, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+ }
+ commitInstallationAt(dir);
+ }
+
+ private void commitInstallationAt(Path dir) throws IOException {
+ // To save storage, delete the source archive on the disk.
+ if (mPath != null) {
+ Files.deleteIfExists(mPath);
+ }
+
+ // Mark the completion
+ Path marker = dir.resolve(InstalledImage.MARKER_FILENAME);
+ Files.createFile(marker);
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallUtils.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallUtils.java
deleted file mode 100644
index 71f2a2d..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallUtils.java
+++ /dev/null
@@ -1,194 +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.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 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/InstalledImage.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.java
new file mode 100644
index 0000000..623fbe4
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.java
@@ -0,0 +1,182 @@
+/*
+ * 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.content.Context;
+import android.os.FileUtils;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+
+/** Collection of files that consist of a VM image. */
+class InstalledImage {
+ private static final String INSTALL_DIRNAME = "linux";
+ private static final String ROOTFS_FILENAME = "root_part";
+ private static final String BACKUP_FILENAME = "root_part_backup";
+ private static final String CONFIG_FILENAME = "vm_config.json";
+ static final String MARKER_FILENAME = "completed";
+
+ public static final long RESIZE_STEP_BYTES = 4 << 20; // 4 MiB
+
+ private final Path mDir;
+ private final Path mRootPartition;
+ private final Path mBackup;
+ private final Path mConfig;
+ private final Path mMarker;
+
+ /** Returns InstalledImage for a given app context */
+ public static InstalledImage getDefault(Context context) {
+ Path installDir = context.getFilesDir().toPath().resolve(INSTALL_DIRNAME);
+ return new InstalledImage(installDir);
+ }
+
+ private InstalledImage(Path dir) {
+ mDir = dir;
+ mRootPartition = dir.resolve(ROOTFS_FILENAME);
+ mBackup = dir.resolve(BACKUP_FILENAME);
+ mConfig = dir.resolve(CONFIG_FILENAME);
+ mMarker = dir.resolve(MARKER_FILENAME);
+ }
+
+ public Path getInstallDir() {
+ return mDir;
+ }
+
+ /** Tests if this InstalledImage is actually installed. */
+ public boolean isInstalled() {
+ return Files.exists(mMarker);
+ }
+
+ /** Fully understalls this InstalledImage by deleting everything. */
+ public void uninstallFully() throws IOException {
+ FileUtils.deleteContentsAndDir(mDir.toFile());
+ }
+
+ /** Returns the path to the VM config file. */
+ public Path getConfigPath() {
+ return mConfig;
+ }
+
+ public Path uninstallAndBackup() throws IOException {
+ Files.delete(mMarker);
+ Files.move(mRootPartition, mBackup, StandardCopyOption.REPLACE_EXISTING);
+ return mBackup;
+ }
+
+ public Path getBackupFile() {
+ return mBackup;
+ }
+
+ public boolean hasBackup() {
+ return Files.exists(mBackup);
+ }
+
+ public void deleteBackup() throws IOException {
+ Files.deleteIfExists(mBackup);
+ }
+
+ public long getSize() throws IOException {
+ return roundUp(Files.size(mRootPartition));
+ }
+
+ public long getSmallestSizePossible() throws IOException {
+ runE2fsck(mRootPartition);
+ String p = mRootPartition.toAbsolutePath().toString();
+ String result = runCommand("/system/bin/resize2fs", "-P", p);
+ // The return value is the number of 4k block
+ try {
+ long minSize =
+ Long.parseLong(result.lines().toArray(String[]::new)[1].substring(42))
+ * 4
+ * 1024;
+ return roundUp(minSize);
+ } catch (NumberFormatException e) {
+ throw new IOException(e);
+ }
+ }
+
+ public long resize(long desiredSize) throws IOException {
+ desiredSize = roundUp(desiredSize);
+ final long curSize = getSize();
+
+ if (desiredSize == curSize) {
+ return desiredSize;
+ }
+
+ runE2fsck(mRootPartition);
+ if (desiredSize > curSize) {
+ allocateSpace(mRootPartition, desiredSize);
+ }
+ resizeFilesystem(mRootPartition, desiredSize);
+ return getSize();
+ }
+
+ private static void allocateSpace(Path path, long sizeInBytes) throws IOException {
+ try {
+ RandomAccessFile raf = new RandomAccessFile(path.toFile(), "rw");
+ FileDescriptor fd = raf.getFD();
+ Os.posix_fallocate(fd, 0, sizeInBytes);
+ raf.close();
+ Log.d(TAG, "Allocated space to: " + sizeInBytes + " bytes");
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to allocate space", e);
+ throw new IOException("Failed to allocate space", e);
+ }
+ }
+
+ private static void runE2fsck(Path path) throws IOException {
+ String p = path.toAbsolutePath().toString();
+ runCommand("/system/bin/e2fsck", "-y", "-f", p);
+ Log.d(TAG, "e2fsck completed: " + path);
+ }
+
+ private static void resizeFilesystem(Path path, long sizeInBytes) throws IOException {
+ long sizeInMB = sizeInBytes / (1024 * 1024);
+ if (sizeInMB == 0) {
+ Log.e(TAG, "Invalid size: " + sizeInBytes + " bytes");
+ throw new IllegalArgumentException("Size cannot be zero MB");
+ }
+ String sizeArg = sizeInMB + "M";
+ String p = path.toAbsolutePath().toString();
+ runCommand("/system/bin/resize2fs", p, sizeArg);
+ Log.d(TAG, "resize2fs completed: " + path + ", size: " + sizeArg);
+ }
+
+ private static String runCommand(String... command) throws IOException {
+ try {
+ Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
+ process.waitFor();
+ return new String(process.getInputStream().readAllBytes());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IOException("Command interrupted", e);
+ }
+ }
+
+ private static long roundUp(long bytes) {
+ // Round up every diskSizeStep MB
+ return (long) Math.ceil(((double) bytes) / RESIZE_STEP_BYTES) * RESIZE_STEP_BYTES;
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
index 52ef3d4..1c62572 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
@@ -41,6 +41,7 @@
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.android.material.snackbar.Snackbar;
+import java.io.IOException;
import java.lang.ref.WeakReference;
public class InstallerActivity extends BaseActivity {
@@ -63,12 +64,8 @@
mInstallProgressListener = new InstallProgressListener(this);
setContentView(R.layout.activity_installer);
-
- TextView desc = (TextView) findViewById(R.id.installer_desc);
- desc.setText(
- getString(
- R.string.installer_desc_text_format,
- Formatter.formatShortFileSize(this, ESTIMATED_IMG_SIZE_BYTES)));
+ updateSizeEstimation(ESTIMATED_IMG_SIZE_BYTES);
+ measureImageSizeAndUpdateDescription();
mWaitForWifiCheckbox = (CheckBox) findViewById(R.id.installer_wait_for_wifi_checkbox);
mInstallButton = (TextView) findViewById(R.id.installer_install_button);
@@ -85,11 +82,38 @@
}
}
+ private void updateSizeEstimation(long est) {
+ String desc =
+ getString(
+ R.string.installer_desc_text_format,
+ Formatter.formatShortFileSize(this, est));
+ runOnUiThread(
+ () -> {
+ TextView view = (TextView) findViewById(R.id.installer_desc);
+ view.setText(desc);
+ });
+ }
+
+ private void measureImageSizeAndUpdateDescription() {
+ new Thread(
+ () -> {
+ long est;
+ try {
+ est = ImageArchive.getDefault().getSize();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to measure image size.", e);
+ return;
+ }
+ updateSizeEstimation(est);
+ })
+ .start();
+ }
+
@Override
public void onResume() {
super.onResume();
- if (Build.isDebuggable() && InstallUtils.payloadFromExternalStorageExists()) {
+ if (Build.isDebuggable() && ImageArchive.fromSdCard().exists()) {
showSnackbar("Auto installing", Snackbar.LENGTH_LONG);
requestInstall();
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
index 6fd3b5c..c2b3fd4 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
@@ -28,7 +28,6 @@
import android.net.NetworkCapabilities;
import android.os.Build;
import android.os.IBinder;
-import android.os.SELinux;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -36,23 +35,13 @@
import com.android.internal.annotations.GuardedBy;
-import org.apache.commons.compress.archivers.ArchiveEntry;
-import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
-import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
-
-import java.io.BufferedInputStream;
-import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.SocketException;
-import java.net.URL;
import java.net.UnknownHostException;
-import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
import java.util.Arrays;
-import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -64,9 +53,6 @@
? "https://dl.google.com/android/ferrochrome/latest/x86_64/images.tar.gz"
: "https://dl.google.com/android/ferrochrome/latest/aarch64/images.tar.gz";
- private static final String SELINUX_FILE_CONTEXT =
- "u:object_r:virtualizationservice_data_file:";
-
private final Object mLock = new Object();
private Notification mNotification;
@@ -162,9 +148,6 @@
mExecutorService.execute(
() -> {
boolean success = downloadFromSdcard() || downloadFromUrl(isWifiOnly);
- if (success) {
- reLabelImagesSELinuxContext();
- }
stopForeground(STOP_FOREGROUND_REMOVE);
synchronized (mLock) {
@@ -176,34 +159,21 @@
});
}
- private void reLabelImagesSELinuxContext() {
- File payloadFolder = InstallUtils.getInternalStorageDir(this);
-
- // The context should be u:object_r:privapp_data_file:s0:c35,c257,c512,c768
- // and we want to get s0:c35,c257,c512,c768 part
- String level = SELinux.getFileContext(payloadFolder.toString()).split(":", 4)[3];
- String targetContext = SELINUX_FILE_CONTEXT + level;
-
- File[] files = payloadFolder.listFiles();
- for (File file : files) {
- if (file.isFile() &&
- !Objects.equals(SELinux.getFileContext(file.toString()),
- targetContext)) {
- SELinux.setFileContext(file.toString(), targetContext);
- }
- }
- }
-
private boolean downloadFromSdcard() {
+ ImageArchive archive = ImageArchive.fromSdCard();
+
// Installing from sdcard is preferred, but only supported only in debuggable build.
- if (Build.isDebuggable()) {
+ if (Build.isDebuggable() && archive.exists()) {
Log.i(TAG, "trying to install /sdcard/linux/images.tar.gz");
- if (InstallUtils.installImageFromExternalStorage(this)) {
+ Path dest = InstalledImage.getDefault(this).getInstallDir();
+ try {
+ archive.installTo(dest, null);
Log.i(TAG, "image is installed from /sdcard/linux/images.tar.gz");
return true;
+ } catch (IOException e) {
+ Log.i(TAG, "Failed to install /sdcard/linux/images.tar.gz", e);
}
- Log.i(TAG, "Failed to install /sdcard/linux/images.tar.gz");
} else {
Log.i(TAG, "Non-debuggable build doesn't support installation from /sdcard/linux");
}
@@ -229,23 +199,16 @@
return false;
}
- try (BufferedInputStream inputStream =
- new BufferedInputStream(new URL(IMAGE_URL).openStream());
- WifiCheckInputStream wifiInputStream =
- new WifiCheckInputStream(inputStream, isWifiOnly);
- TarArchiveInputStream tar =
- new TarArchiveInputStream(new GzipCompressorInputStream(wifiInputStream))) {
- ArchiveEntry entry;
- Path baseDir = InstallUtils.getInternalStorageDir(this).toPath();
- Files.createDirectories(baseDir);
- while ((entry = tar.getNextEntry()) != null) {
- Path extractTo = baseDir.resolve(entry.getName());
- if (entry.isDirectory()) {
- Files.createDirectories(extractTo);
- } else {
- Files.copy(tar, extractTo, StandardCopyOption.REPLACE_EXISTING);
- }
- }
+ Path dest = InstalledImage.getDefault(this).getInstallDir();
+ try {
+ ImageArchive.fromInternet()
+ .installTo(
+ dest,
+ is -> {
+ WifiCheckInputStream filter = new WifiCheckInputStream(is);
+ filter.setWifiOnly(isWifiOnly);
+ return filter;
+ });
} catch (WifiCheckInputStream.NoWifiException e) {
Log.e(TAG, "Install failed because of Wi-Fi is gone");
notifyError(getString(R.string.installer_error_no_wifi));
@@ -260,12 +223,7 @@
notifyError(getString(R.string.installer_error_unknown));
return false;
}
-
- if (!InstallUtils.resolvePathInVmConfig(this)) {
- notifyError(getString(R.string.installer_error_unknown));
- return false;
- }
- return InstallUtils.createInstalledMarker(this);
+ return true;
}
private void notifyError(String displayText) {
@@ -339,7 +297,7 @@
public boolean isInstalled() {
InstallerService service = ensureServiceConnected();
synchronized (service.mLock) {
- return !service.mIsInstalling && InstallUtils.isImageInstalled(service);
+ return !service.mIsInstalling && InstalledImage.getDefault(service).isInstalled();
}
}
}
@@ -348,11 +306,14 @@
private static final int READ_BYTES = 1024;
private final InputStream mInputStream;
- private final boolean mIsWifiOnly;
+ private boolean mIsWifiOnly;
- public WifiCheckInputStream(InputStream is, boolean isWifiOnly) {
+ public WifiCheckInputStream(InputStream is) {
super();
mInputStream = is;
+ }
+
+ public void setWifiOnly(boolean isWifiOnly) {
mIsWifiOnly = isWifiOnly;
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index bc5d037..deef825 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -35,8 +35,6 @@
import android.os.ConditionVariable;
import android.os.Environment;
import android.provider.Settings;
-import android.system.ErrnoException;
-import android.system.Os;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
@@ -61,11 +59,7 @@
import com.google.android.material.appbar.MaterialToolbar;
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.FileNotFoundException;
import java.io.IOException;
-import java.io.RandomAccessFile;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
@@ -83,6 +77,7 @@
private static final int REQUEST_CODE_INSTALLER = 0x33;
private static final int FONT_SIZE_DEFAULT = 13;
+ private InstalledImage mImage;
private X509Certificate[] mCertificates;
private PrivateKey mPrivateKey;
private WebView mWebView;
@@ -90,7 +85,6 @@
private ConditionVariable mBootCompleted = new ConditionVariable();
private static final int POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE = 101;
private ActivityResultLauncher<Intent> mManageExternalStorageActivityResultLauncher;
- private static int diskSizeStep;
private static final Map<Integer, Integer> BTN_KEY_CODE_MAP =
Map.ofEntries(
Map.entry(R.id.btn_tab, KeyEvent.KEYCODE_TAB),
@@ -110,6 +104,8 @@
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ mImage = InstalledImage.getDefault(this);
+
NotificationManager notificationManager = getSystemService(NotificationManager.class);
if (notificationManager.getNotificationChannel(this.getPackageName()) == null) {
NotificationChannel channel =
@@ -123,8 +119,6 @@
boolean launchInstaller = installIfNecessary();
setContentView(R.layout.activity_headless);
- diskSizeStep = getResources().getInteger(
- R.integer.disk_size_round_up_step_size_in_mb) << 20;
MaterialToolbar toolbar = (MaterialToolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
@@ -196,7 +190,7 @@
public boolean dispatchKeyEvent(KeyEvent event) {
if (Build.isDebuggable() && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
if (event.getAction() == KeyEvent.ACTION_UP) {
- launchErrorActivity(new Exception("Debug: KeyEvent.KEYCODE_UNKNOWN"));
+ ErrorActivity.start(this, new Exception("Debug: KeyEvent.KEYCODE_UNKNOWN"));
}
return true;
}
@@ -334,80 +328,6 @@
.start();
}
- private void diskResize(File file, long sizeInBytes) throws IOException {
- try {
- if (sizeInBytes == 0) {
- return;
- }
- String filePath = file.getAbsolutePath();
- Log.d(TAG, "Disk-resize in progress for partition: " + filePath);
-
- long currentSize = Os.stat(filePath).st_size;
- runE2fsck(filePath);
- if (sizeInBytes > currentSize) {
- allocateSpace(file, sizeInBytes);
- }
-
- resizeFilesystem(filePath, sizeInBytes);
- } catch (ErrnoException e) {
- Log.e(TAG, "ErrnoException during disk resize", e);
- throw new IOException("ErrnoException during disk resize", e);
- } catch (IOException e) {
- Log.e(TAG, "Failed to resize disk", e);
- throw e;
- }
- }
-
- private static void allocateSpace(File file, long sizeInBytes) throws IOException {
- try {
- RandomAccessFile raf = new RandomAccessFile(file, "rw");
- FileDescriptor fd = raf.getFD();
- Os.posix_fallocate(fd, 0, sizeInBytes);
- raf.close();
- Log.d(TAG, "Allocated space to: " + sizeInBytes + " bytes");
- } catch (ErrnoException e) {
- Log.e(TAG, "Failed to allocate space", e);
- throw new IOException("Failed to allocate space", e);
- }
- }
-
- private static void runE2fsck(String filePath) throws IOException {
- try {
- runCommand("/system/bin/e2fsck", "-y", "-f", filePath);
- Log.d(TAG, "e2fsck completed: " + filePath);
- } catch (IOException e) {
- Log.e(TAG, "Failed to run e2fsck", e);
- throw e;
- }
- }
-
- private static void resizeFilesystem(String filePath, long sizeInBytes) throws IOException {
- long sizeInMB = sizeInBytes / (1024 * 1024);
- if (sizeInMB == 0) {
- Log.e(TAG, "Invalid size: " + sizeInBytes + " bytes");
- throw new IllegalArgumentException("Size cannot be zero MB");
- }
- String sizeArg = sizeInMB + "M";
- try {
- runCommand("/system/bin/resize2fs", filePath, sizeArg);
- Log.d(TAG, "resize2fs completed: " + filePath + ", size: " + sizeArg);
- } catch (IOException e) {
- Log.e(TAG, "Failed to run resize2fs", e);
- throw e;
- }
- }
-
- private static String runCommand(String... command) throws IOException {
- try {
- Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
- process.waitFor();
- return new String(process.getInputStream().readAllBytes());
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new IOException("Command interrupted", e);
- }
- }
-
private static void waitUntilVmStarts() {
InetAddress addr = null;
try {
@@ -445,7 +365,8 @@
@Override
public void onVmError() {
Log.i(TAG, "onVmError()");
- launchErrorActivity(new Exception("onVmError"));
+ // TODO: error cause is too simple.
+ ErrorActivity.start(this, new Exception("onVmError"));
}
@Override
@@ -502,17 +423,10 @@
}
}
- private void launchErrorActivity(Exception e) {
- Intent intent = new Intent(this, ErrorActivity.class);
- intent.putExtra(ErrorActivity.EXTRA_CAUSE, e);
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
- this.startActivity(intent);
- }
-
private boolean installIfNecessary() {
// If payload from external storage exists(only for debuggable build) or there is no
// installed image, launch installer activity.
- if (!InstallUtils.isImageInstalled(this)) {
+ if (!mImage.isInstalled()) {
Intent intent = new Intent(this, InstallerActivity.class);
startActivityForResult(intent, REQUEST_CODE_INSTALLER);
return true;
@@ -521,16 +435,17 @@
}
private void startVm() {
- if (!InstallUtils.isImageInstalled(this)) {
+ InstalledImage image = InstalledImage.getDefault(this);
+ if (!image.isInstalled()) {
return;
}
- resizeDiskIfNecessary();
+ resizeDiskIfNecessary(image);
Intent tapIntent = new Intent(this, MainActivity.class);
tapIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
- PendingIntent tapPendingIntent = PendingIntent.getActivity(this, 0, tapIntent,
- PendingIntent.FLAG_IMMUTABLE);
+ PendingIntent tapPendingIntent =
+ PendingIntent.getActivity(this, 0, tapIntent, PendingIntent.FLAG_IMMUTABLE);
Intent settingsIntent = new Intent(this, SettingsActivity.class);
settingsIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
@@ -586,52 +501,25 @@
return mBootCompleted.block(timeoutMillis);
}
- private static long roundUpDiskSize(long diskSize) {
- // Round up every diskSizeStep MB
- return (long) Math.ceil(((double) diskSize) / diskSizeStep) * diskSizeStep;
- }
+ private void resizeDiskIfNecessary(InstalledImage image) {
+ String prefKey = getString(R.string.preference_file_key);
+ String key = getString(R.string.preference_disk_size_key);
+ SharedPreferences sharedPref = this.getSharedPreferences(prefKey, Context.MODE_PRIVATE);
+ long newSize = sharedPref.getLong(key, -1);
- public static long getMinFilesystemSize(File file) throws IOException, NumberFormatException {
- try {
- runE2fsck(file.getAbsolutePath());
- String result = runCommand("/system/bin/resize2fs", "-P", file.getAbsolutePath());
- // The return value is the number of 4k block
- long minSize = Long.parseLong(
- result.lines().toArray(String[]::new)[1].substring(42)) * 4 * 1024;
- return roundUpDiskSize(minSize);
- } catch (IOException | NumberFormatException e) {
- Log.e(TAG, "Failed to get filesystem size", e);
- throw e;
+ // No preferred size. Don't resize.
+ if (newSize == -1) {
+ return;
}
- }
- private static long getFilesystemSize(File file) throws ErrnoException {
- return Os.stat(file.getAbsolutePath()).st_size;
- }
-
- private void resizeDiskIfNecessary() {
try {
- File file = InstallUtils.getRootfsFile(this);
- SharedPreferences sharedPref = this.getSharedPreferences(
- getString(R.string.preference_file_key), Context.MODE_PRIVATE);
- SharedPreferences.Editor editor = sharedPref.edit();
-
- long currentDiskSize = getFilesystemSize(file);
- long newSizeInBytes = sharedPref.getLong(getString(R.string.preference_disk_size_key),
- roundUpDiskSize(currentDiskSize));
- editor.putLong(getString(R.string.preference_disk_size_key), newSizeInBytes);
- editor.apply();
-
- Log.d(TAG, "Current disk size: " + currentDiskSize);
- Log.d(TAG, "Targeting disk size: " + newSizeInBytes);
-
- if (newSizeInBytes != currentDiskSize) {
- diskResize(file, newSizeInBytes);
- }
- } catch (FileNotFoundException e) {
- Log.d(TAG, "No partition file");
- } catch (IOException | ErrnoException | NumberFormatException e) {
+ Log.d(TAG, "Resizing disk to " + newSize + " bytes");
+ newSize = image.resize(newSize);
+ } catch (IOException e) {
Log.e(TAG, "Failed to resize disk", e);
+ return;
}
+
+ sharedPref.edit().putLong(key, newSize).apply();
}
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
index 817808f..442f896 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
@@ -55,9 +55,9 @@
0
)
).toFloat();
- val partition = InstallUtils.getRootfsFile(this)
+ val image = InstalledImage.getDefault(this)
val minDiskSizeMb =
- bytesToMb(MainActivity.getMinFilesystemSize(partition)).toFloat()
+ bytesToMb(image.getSmallestSizePossible()).toFloat()
.coerceAtMost(diskSizeMb)
val diskSizeText = findViewById<TextView>(R.id.settings_disk_resize_resize_gb_assigned)
@@ -141,4 +141,4 @@
}
return summary
}
-}
\ No newline at end of file
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
index 1b39ff0..a332a9d 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
@@ -47,7 +47,7 @@
HashSet<String>()
)
- for (port in ports!!) {
+ for (port in ports!!.sortedWith(compareBy( { it.toInt() } ))) {
val enabled =
sharedPref.getBoolean(
getString(R.string.preference_forwarding_port_is_enabled) + port,
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
index e291b57..00730ff 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
@@ -27,6 +27,7 @@
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import java.io.IOException
+import java.nio.file.Files
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -55,7 +56,8 @@
dialog.show()
}
val resetBackupCard = findViewById<View>(R.id.settings_recovery_reset_backup_card)
- resetBackupCard.isVisible = InstallUtils.getBackupFile(this).exists()
+
+ resetBackupCard.isVisible = InstalledImage.getDefault(this).hasBackup()
resetBackupCard.setOnClickListener {
val dialog = MaterialAlertDialogBuilder(this)
@@ -73,7 +75,9 @@
}
private fun removeBackup(): Unit {
- if (!InstallUtils.getBackupFile(this@SettingsRecoveryActivity).delete()) {
+ try {
+ InstalledImage.getDefault(this).deleteBackup()
+ } catch (e: IOException) {
Snackbar.make(
findViewById(android.R.id.content),
R.string.settings_recovery_error_during_removing_backup,
@@ -85,12 +89,14 @@
private fun uninstall(backupRootfs: Boolean): Unit {
var backupDone = false
+ val image = InstalledImage.getDefault(this)
try {
if (backupRootfs) {
- InstallUtils.backupRootFs(this@SettingsRecoveryActivity)
+ image.uninstallAndBackup()
backupDone = true
+ } else {
+ image.uninstallFully()
}
- InstallUtils.deleteInstallation(this@SettingsRecoveryActivity)
} catch (e: IOException) {
val errorMsgId = if (backupRootfs && !backupDone) R.string.settings_recovery_error_due_to_backup
else R.string.settings_recovery_error;
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
index 274f423..efee62f 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
@@ -244,6 +244,7 @@
info.getBoundsInScreen(rect);
if (rect.width() == 0) {
info.setText(null);
+ info.setContentDescription(getString(R.string.empty_line));
}
info.setScreenReaderFocusable(false);
break;
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
index 652af8e..c2d224a 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
@@ -35,6 +35,7 @@
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;
@@ -50,6 +51,7 @@
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.Set;
@@ -151,13 +153,12 @@
}
mExecutorService = Executors.newCachedThreadPool();
- ConfigJson json = ConfigJson.from(InstallUtils.getVmConfigPath(this));
+ InstalledImage image = InstalledImage.getDefault(this);
+ ConfigJson json = ConfigJson.from(this, image.getConfigPath());
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()));
+ if (overrideConfigIfNecessary(customImageConfigBuilder)) {
configBuilder.setCustomImageConfig(customImageConfigBuilder.build());
}
VirtualMachineConfig config = configBuilder.build();
@@ -201,6 +202,33 @@
return START_NOT_STICKY;
}
+ 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() {
ServerInterceptor interceptor =
new ServerInterceptor() {
diff --git a/android/TerminalApp/res/values/config.xml b/android/TerminalApp/res/values/config.xml
index 9d2456c..ea762fc 100644
--- a/android/TerminalApp/res/values/config.xml
+++ b/android/TerminalApp/res/values/config.xml
@@ -17,7 +17,6 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="preference_file_key" translatable="false">com.android.virtualization.terminal.PREFERENCE_FILE_KEY</string>
<string name="preference_disk_size_key" translatable="false">PREFERENCE_DISK_SIZE_KEY</string>
- <string name="preference_min_disk_size_key" translatable="false">PREFERENCE_MIN_DISK_SIZE_KEY</string>
<string name="preference_forwarding_ports" translatable="false">PREFERENCE_FORWARDING_PORTS</string>
<string name="preference_forwarding_port_is_enabled" translatable="false">PREFERENCE_FORWARDING_PORT_IS_ENABLED_</string>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
index 851eaab..da8ca84 100644
--- a/android/TerminalApp/res/values/strings.xml
+++ b/android/TerminalApp/res/values/strings.xml
@@ -132,4 +132,7 @@
<string name="service_notification_content">Click to open the terminal</string>
<!-- Notification action button for closing the virtual machine [CHAR LIMIT=20] -->
<string name="service_notification_quit_action">Close</string>
+
+ <!-- VirGL is the name of hardware acceleration for VM, the name is supposed not to be translated. [CHAR LIMIT=20] -->
+ <string name="virgl_enabled">VirGL is enabled</string>
</resources>
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index 1cae344..9a733b6 100644
--- a/android/virtmgr/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -573,41 +573,42 @@
.or_binder_exception(ExceptionCode::SECURITY)?;
}
- // Check if partition images are labeled incorrectly. This is to prevent random images
- // which are not protected by the Android Verified Boot (e.g. bits downloaded by apps) from
- // being loaded in a pVM. This applies to everything but the instance image in the raw
- // config, and everything but the non-executable, generated partitions in the app
- // config.
- config
- .disks
- .iter()
- .flat_map(|disk| disk.partitions.iter())
- .filter(|partition| {
- if is_app_config {
- !is_safe_app_partition(&partition.label)
- } else {
- !is_safe_raw_partition(&partition.label)
- }
- })
- .try_for_each(check_label_for_partition)
- .or_service_specific_exception(-1)?;
+ let kernel = maybe_clone_file(&config.kernel)?;
+ let initrd = maybe_clone_file(&config.initrd)?;
+
+ if config.protectedVm {
+ // Fail fast with a meaningful error message in case device doesn't support pVMs.
+ check_protected_vm_is_supported()?;
+
+ // In a protected VM, we require custom kernels to come from a trusted source
+ // (b/237054515).
+ check_label_for_kernel_files(&kernel, &initrd).or_service_specific_exception(-1)?;
+
+ // Check if partition images are labeled incorrectly. This is to prevent random images
+ // which are not protected by the Android Verified Boot (e.g. bits downloaded by apps)
+ // from being loaded in a pVM. This applies to everything but the instance image in the
+ // raw config, and everything but the non-executable, generated partitions in the app
+ // config.
+ config
+ .disks
+ .iter()
+ .flat_map(|disk| disk.partitions.iter())
+ .filter(|partition| {
+ if is_app_config {
+ !is_safe_app_partition(&partition.label)
+ } else {
+ !is_safe_raw_partition(&partition.label)
+ }
+ })
+ .try_for_each(check_label_for_partition)
+ .or_service_specific_exception(-1)?;
+ }
// Check if files for payloads and bases are NOT coming from /vendor and /odm, as they may
// have unstable interfaces.
// TODO(b/316431494): remove once Treble interfaces are stabilized.
check_partitions_for_files(config).or_service_specific_exception(-1)?;
- let kernel = maybe_clone_file(&config.kernel)?;
- let initrd = maybe_clone_file(&config.initrd)?;
-
- if config.protectedVm {
- // In a protected VM, we require custom kernels to come from a trusted source
- // (b/237054515).
- check_label_for_kernel_files(&kernel, &initrd).or_service_specific_exception(-1)?;
- // Fail fast with a meaningful error message in case device doesn't support pVMs.
- check_protected_vm_is_supported()?;
- }
-
let zero_filler_path = temporary_directory.join("zero.img");
write_zero_filler(&zero_filler_path)
.context("Failed to make composite image")
diff --git a/build/debian/build.sh b/build/debian/build.sh
index 59a98b6..8f232aa 100755
--- a/build/debian/build.sh
+++ b/build/debian/build.sh
@@ -144,7 +144,7 @@
build_ttyd() {
local ttyd_version=1.7.7
local url="https://github.com/tsl0922/ttyd/archive/refs/tags/${ttyd_version}.tar.gz"
- cp -r $(dirname $0)/ttyd ${workdir}/ttyd
+ cp -r "$(dirname "$0")/ttyd" "${workdir}/ttyd"
pushd "${workdir}" > /dev/null
wget "${url}" -O - | tar xz
@@ -152,7 +152,7 @@
pushd "$workdir/ttyd-${ttyd_version}" > /dev/null
bash -c "env BUILD_TARGET=${arch} ./scripts/cross-build.sh"
mkdir -p "${dst}/files/usr/local/bin/ttyd"
- cp /tmp/stage/${arch}-linux-musl/bin/ttyd "${dst}/files/usr/local/bin/ttyd/AVF"
+ cp "/tmp/stage/${arch}-linux-musl/bin/ttyd" "${dst}/files/usr/local/bin/ttyd/AVF"
chmod 777 "${dst}/files/usr/local/bin/ttyd/AVF"
mkdir -p "${dst}/files/usr/share/doc/ttyd"
cp LICENSE "${dst}/files/usr/share/doc/ttyd/copyright"
@@ -161,8 +161,10 @@
}
copy_android_config() {
- local src="$(dirname "$0")/fai_config"
- local dst="${config_space}"
+ local src
+ local dst
+ src="$(dirname "$0")/fai_config"
+ dst="${config_space}"
cp -R "${src}"/* "${dst}"
cp "$(dirname "$0")/image.yaml" "${resources_dir}"
@@ -181,14 +183,21 @@
extract_partitions() {
root_partition_num=1
+ bios_partition_num=14
efi_partition_num=15
loop=$(losetup -f --show --partscan $built_image)
- dd if=${loop}p$root_partition_num of=root_part
- dd if=${loop}p$efi_partition_num of=efi_part
- losetup -d ${loop}
+ dd if="${loop}p$root_partition_num" of=root_part
+ if [[ "$arch" == "x86_64" ]]; then
+ dd if="${loop}p$bios_partition_num" of=bios_part
+ fi
+ dd if="${loop}p$efi_partition_num" of=efi_part
+ losetup -d "${loop}"
sed -i "s/{root_part_guid}/$(sfdisk --part-uuid $built_image $root_partition_num)/g" vm_config.json
+ if [[ "$arch" == "x86_64" ]]; then
+ sed -i "s/{bios_part_guid}/$(sfdisk --part-uuid $built_image $bios_partition_num)/g" vm_config.json
+ fi
sed -i "s/{efi_part_guid}/$(sfdisk --part-uuid $built_image $efi_partition_num)/g" vm_config.json
}
@@ -217,27 +226,28 @@
fdisk -l "${built_image}"
images=()
-cp $(dirname $0)/vm_config.json.${arch} vm_config.json
+cp "$(dirname "$0")/vm_config.json.${arch}" vm_config.json
+
+extract_partitions
if [[ "$arch" == "aarch64" ]]; then
- extract_partitions
images+=(
root_part
efi_part
)
-fi
-
# TODO(b/365955006): remove these lines when uboot supports x86_64 EFI application
-if [[ "$arch" == "x86_64" ]]; then
+elif [[ "$arch" == "x86_64" ]]; then
virt-get-kernel -a "${built_image}"
mv vmlinuz* vmlinuz
mv initrd.img* initrd.img
images+=(
- "${built_image}"
+ boot_part
+ root_part
+ efi_part
vmlinuz
initrd.img
)
fi
# --sparse option isn't supported in apache-commons-compress
-tar czv -f images.tar.gz ${images[@]} vm_config.json
+tar czv -f images.tar.gz "${images[@]}" vm_config.json
diff --git a/build/debian/fai_config/scripts/AVF/10-systemd b/build/debian/fai_config/scripts/AVF/10-systemd
index a514299..94838bc 100755
--- a/build/debian/fai_config/scripts/AVF/10-systemd
+++ b/build/debian/fai_config/scripts/AVF/10-systemd
@@ -10,3 +10,5 @@
ln -s /etc/systemd/system/forwarder_guest_launcher.service $target/etc/systemd/system/multi-user.target.wants/forwarder_guest_launcher.service
ln -s /etc/systemd/system/virtiofs_internal.service $target/etc/systemd/system/multi-user.target.wants/virtiofs_internal.service
ln -s /etc/systemd/system/backup_mount.service $target/etc/systemd/system/multi-user.target.wants/backup_mount.service
+
+sed -i 's/#LLMNR=yes/LLMNR=no/' $target/etc/systemd/resolved.conf
diff --git a/build/debian/vm_config.json.x86_64 b/build/debian/vm_config.json.x86_64
index d338080..4d31105 100644
--- a/build/debian/vm_config.json.x86_64
+++ b/build/debian/vm_config.json.x86_64
@@ -2,8 +2,26 @@
"name": "debian",
"disks": [
{
- "image": "$PAYLOAD_DIR/image.raw",
- "partitions": [],
+ "partitions": [
+ {
+ "label": "ROOT",
+ "path": "$PAYLOAD_DIR/root_part",
+ "writable": true,
+ "guid": "{root_part_guid}"
+ },
+ {
+ "label": "BIOS",
+ "path": "$PAYLOAD_DIR/bios_part",
+ "writable": true,
+ "guid": "{root_part_guid}"
+ },
+ {
+ "label": "EFI",
+ "path": "$PAYLOAD_DIR/efi_part",
+ "writable": false,
+ "guid": "{efi_part_guid}"
+ }
+ ],
"writable": true
}
],
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index eaff2a4..7597131 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -42,4 +42,8 @@
(or `localhost:6080` if port forwarding is enabled.)
`weston` with VNC backend might be another option, but it isn't available in
-Debian package repository for bookworm.
\ No newline at end of file
+Debian package repository for bookworm.
+
+## Hardware accelration
+If the file `/sdcard/linux/virglrenderer` exists on the device, it enables VirGL for VM.
+This requires enabling ANGLE for the Terminal app. (https://chromium.googlesource.com/angle/angle.git/+/HEAD/doc/DevSetupAndroid.md)
diff --git a/guest/forwarder_guest_launcher/src/main.rs b/guest/forwarder_guest_launcher/src/main.rs
index 1da37b4..f6944d6 100644
--- a/guest/forwarder_guest_launcher/src/main.rs
+++ b/guest/forwarder_guest_launcher/src/main.rs
@@ -35,6 +35,7 @@
}
const NON_PREVILEGED_PORT_RANGE_START: i32 = 1024;
+const TTYD_PORT: i32 = 7681;
const TCPSTATES_IP_4: i8 = 4;
const TCPSTATES_STATE_CLOSE: &str = "CLOSE";
const TCPSTATES_STATE_LISTEN: &str = "LISTEN";
@@ -108,6 +109,10 @@
Ok(())
}
+fn is_forwardable_port(port: i32) -> bool {
+ port >= NON_PREVILEGED_PORT_RANGE_START && port != TTYD_PORT
+}
+
async fn report_active_ports(
mut client: DebianServiceClient<Channel>,
) -> Result<(), Box<dyn std::error::Error>> {
@@ -130,7 +135,7 @@
.map(|x| x.socket)
.filter(|x| x.is_ipv4())
.map(|x| x.port().into())
- .filter(|x| *x >= NON_PREVILEGED_PORT_RANGE_START) // Ignore privileged ports
+ .filter(|x| is_forwardable_port(*x))
.collect();
send_active_ports_report(listening_ports.clone(), &mut client).await?;
@@ -140,7 +145,7 @@
if row.ip != TCPSTATES_IP_4 {
continue;
}
- if row.lport < NON_PREVILEGED_PORT_RANGE_START {
+ if !is_forwardable_port(row.lport) {
continue;
}
if row.rport > 0 {
diff --git a/tests/Terminal/src/com/android/virtualization/terminal/TerminalAppTest.java b/tests/Terminal/src/com/android/virtualization/terminal/TerminalAppTest.java
index 3c0461d..42c31e3 100644
--- a/tests/Terminal/src/com/android/virtualization/terminal/TerminalAppTest.java
+++ b/tests/Terminal/src/com/android/virtualization/terminal/TerminalAppTest.java
@@ -33,6 +33,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -88,7 +89,7 @@
}
@After
- public void tearDown() {
- InstallUtils.deleteInstallation(mTargetContext);
+ public void tearDown() throws IOException {
+ InstalledImage.getDefault(mTargetContext).uninstallFully();
}
}