Add InstalledImage
... and move resize functions to the class.
Bug: N/A
Test: N/A
Change-Id: Ic6affe75733b219a186613700cd7b966fcc74e11
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..deb0c14
--- /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 backupFile() {
+ 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/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index ded186f..6740778 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,12 @@
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 +78,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 +86,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 +105,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 +120,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 +329,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 +427,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,11 +436,12 @@
}
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);
@@ -579,53 +502,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
+}