Replace InstalUtils with ImageArchive and InstalledImage

Bug: N/A
Test: N/A
Change-Id: I7a4ef524ec90c55127f84067f5b51d577774c78d
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/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
index deb0c14..623fbe4 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.java
@@ -85,7 +85,7 @@
         return mBackup;
     }
 
-    public Path backupFile() {
+    public Path getBackupFile() {
         return mBackup;
     }
 
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 6740778..deef825 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -59,7 +59,6 @@
 
 import com.google.android.material.appbar.MaterialToolbar;
 
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.MalformedURLException;
@@ -445,8 +444,8 @@
 
         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);
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/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();
     }
 }