Merge "Import translations. DO NOT MERGE ANYWHERE" into main
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java b/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java
index 5cf123e..ab03049 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java
@@ -87,7 +87,7 @@
private static String replaceKeywords(Reader r, Context context) throws IOException {
Map<String, String> rules = new HashMap<>();
- rules.put("\\$PAYLOAD_DIR", InstallUtils.getInternalStorageDir(context).toString());
+ 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();
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
index 54aa07a..b2a2085 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
@@ -137,15 +137,15 @@
* 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 path, Function<InputStream, InputStream> filter) throws IOException {
+ 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(path);
+ Files.createDirectories(dir);
ArchiveEntry entry;
while ((entry = tarStream.getNextEntry()) != null) {
- Path to = path.resolve(entry.getName());
+ Path to = dir.resolve(entry.getName());
if (Files.isDirectory(to)) {
Files.createDirectories(to);
} else {
@@ -153,13 +153,17 @@
}
}
}
- postInstall();
+ commitInstallationAt(dir);
}
- // To save storage, delete the source archive on the disk.
- private void postInstall() throws IOException {
+ 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 1b6da6c..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallUtils.java
+++ /dev/null
@@ -1,149 +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;
-
-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 Path getVmConfigPath(Context context) {
- return getInternalStorageDir(context).resolve(VM_CONFIG_FILENAME);
- }
-
- public static boolean isImageInstalled(Context context) {
- return Files.exists(getInstallationCompletedPath(context));
- }
-
- public static void backupRootFs(Context context) throws IOException {
- Files.move(
- getRootfsFile(context),
- getBackupFile(context),
- StandardCopyOption.REPLACE_EXISTING);
- }
-
- public static boolean createInstalledMarker(Context context) {
- try {
- Path path = getInstallationCompletedPath(context);
- Files.createFile(path);
- return true;
- } 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).toFile());
- }
-
- 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 Path getInternalStorageDir(Context context) {
- return context.getFilesDir().toPath().resolve(PAYLOAD_DIR);
- }
-
- public static Path getBackupFile(Context context) {
- return context.getFilesDir().toPath().resolve(BACKUP_FILENAME);
- }
-
- private static Path getInstallationCompletedPath(Context context) {
- return getInternalStorageDir(context).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;
- }
-
- // 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);
- }
-
- public static Path getRootfsFile(Context context) throws FileNotFoundException {
- Path path = getInternalStorageDir(context).resolve(ROOTFS_FILENAME);
- if (!Files.exists(path)) {
- Log.d(TAG, path.toString() + " - file not found");
- throw new FileNotFoundException("File not found: " + ROOTFS_FILENAME);
- }
- return path;
- }
-}
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 69b5ee7..1c62572 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
@@ -113,7 +113,7 @@
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 a8b4ca2..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;
@@ -171,15 +160,20 @@
}
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");
}
@@ -205,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);
- 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));
@@ -236,8 +223,7 @@
notifyError(getString(R.string.installer_error_unknown));
return false;
}
-
- return InstallUtils.createInstalledMarker(this);
+ return true;
}
private void notifyError(String displayText) {
@@ -311,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();
}
}
}
@@ -320,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 ded186f..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,16 +59,11 @@
import com.google.android.material.appbar.MaterialToolbar;
-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;
import java.net.UnknownHostException;
-import java.nio.file.Files;
-import java.nio.file.Path;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
@@ -84,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;
@@ -91,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),
@@ -111,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 =
@@ -124,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);
@@ -335,78 +328,6 @@
.start();
}
- private void diskResize(Path path, long sizeInBytes) throws IOException {
- try {
- if (sizeInBytes == 0) {
- return;
- }
- Log.d(TAG, "Disk-resize in progress for partition: " + path);
-
- long currentSize = Files.size(path);
- runE2fsck(path);
- if (sizeInBytes > currentSize) {
- allocateSpace(path, sizeInBytes);
- }
-
- resizeFilesystem(path, sizeInBytes);
- } catch (IOException e) {
- Log.e(TAG, "Failed to resize disk", e);
- throw e;
- }
- }
-
- 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 {
- try {
- String p = path.toAbsolutePath().toString();
- runCommand("/system/bin/e2fsck", "-y", "-f", p);
- Log.d(TAG, "e2fsck completed: " + path);
- } catch (IOException e) {
- Log.e(TAG, "Failed to run e2fsck", e);
- throw e;
- }
- }
-
- 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";
- try {
- String p = path.toAbsolutePath().toString();
- runCommand("/system/bin/resize2fs", p, sizeArg);
- Log.d(TAG, "resize2fs completed: " + path + ", 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 {
@@ -505,7 +426,7 @@
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;
@@ -514,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);
@@ -579,53 +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(Path path) throws IOException, NumberFormatException {
- try {
- runE2fsck(path);
- String p = path.toAbsolutePath().toString();
- String result = runCommand("/system/bin/resize2fs", "-P", p);
- // 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(Path fsPath) throws ErrnoException {
- return Os.stat(fsPath.toAbsolutePath().toString()).st_size;
- }
-
- private void resizeDiskIfNecessary() {
try {
- Path 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/SettingsRecoveryActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
index 4b6bf96..00730ff 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
@@ -56,7 +56,8 @@
dialog.show()
}
val resetBackupCard = findViewById<View>(R.id.settings_recovery_reset_backup_card)
- resetBackupCard.isVisible = Files.exists(InstallUtils.getBackupFile(this))
+
+ resetBackupCard.isVisible = InstalledImage.getDefault(this).hasBackup()
resetBackupCard.setOnClickListener {
val dialog = MaterialAlertDialogBuilder(this)
@@ -74,9 +75,8 @@
}
private fun removeBackup(): Unit {
- val file = InstallUtils.getBackupFile(this@SettingsRecoveryActivity)
try {
- Files.deleteIfExists(file)
+ InstalledImage.getDefault(this).deleteBackup()
} catch (e: IOException) {
Snackbar.make(
findViewById(android.R.id.content),
@@ -89,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/VmLauncherService.java b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
index b8a427c..c2d224a 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
@@ -153,7 +153,8 @@
}
mExecutorService = Executors.newCachedThreadPool();
- ConfigJson json = ConfigJson.from(this, 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);
@@ -219,9 +220,10 @@
changed = true;
}
- Path backupFile = InstallUtils.getBackupFile(this);
- if (Files.exists(backupFile)) {
- builder.addDisk(Disk.RWDisk(backupFile.toString()));
+ InstalledImage image = InstalledImage.getDefault(this);
+ if (image.hasBackup()) {
+ Path backup = image.getBackupFile();
+ builder.addDisk(Disk.RWDisk(backup.toString()));
changed = true;
}
return changed;
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/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/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();
}
}