Convert ImageArchive to kotlin

Bug: 383243644
Test: install vm image
Change-Id: I935e2fafd41254c11f96a49914a6fb4a714e40fd
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
deleted file mode 100644
index 626b2a7..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
+++ /dev/null
@@ -1,175 +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.os.Build;
-import android.os.Environment;
-import android.util.Log;
-
-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 BUILD_TAG = "latest";
-    private static final String HOST_URL = "https://dl.google.com/android/ferrochrome/" + BUILD_TAG;
-
-    // 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 {
-        String source = mPath != null ? mPath.toString() : mUrl.toString();
-        Log.d(TAG, "Installing. source: " + source + ", destination: " + dir.toString());
-        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);
-                    continue;
-                }
-                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/ImageArchive.kt b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt
new file mode 100644
index 0000000..e84250b
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt
@@ -0,0 +1,187 @@
+/*
+ * 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 android.util.Log
+import com.android.virtualization.terminal.MainActivity.TAG
+import java.io.BufferedInputStream
+import java.io.FileInputStream
+import java.io.IOException
+import java.io.InputStream
+import java.lang.RuntimeException
+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.function.Function
+import org.apache.commons.compress.archivers.ArchiveEntry
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
+
+/**
+ * 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.
+ */
+internal class ImageArchive {
+    // Only one can be non-null
+    private sealed class Source<out A, out B>
+
+    private data class UrlSource<out Url>(val value: Url) : Source<Url, Nothing>()
+
+    private data class PathSource<out Path>(val value: Path) : Source<Nothing, Path>()
+
+    private val source: Source<URL, Path>
+
+    private constructor(url: URL) {
+        source = UrlSource(url)
+    }
+
+    private constructor(path: Path) {
+        source = PathSource(path)
+    }
+
+    /** Tests if ImageArchive exists on the medium. */
+    fun exists(): Boolean {
+        return when (source) {
+            is UrlSource -> true
+            is PathSource -> Files.exists(source.value)
+        }
+    }
+
+    /** Returns size of the archive in bytes */
+    @Throws(IOException::class)
+    fun getSize(): Long {
+        check(exists()) { "Cannot get size of non existing archive" }
+        return when (source) {
+            is UrlSource -> {
+                val conn = source.value.openConnection() as HttpURLConnection
+                try {
+                    conn.requestMethod = "HEAD"
+                    conn.getInputStream()
+                    return conn.contentLength.toLong()
+                } finally {
+                    conn.disconnect()
+                }
+            }
+            is PathSource -> Files.size(source.value)
+        }
+    }
+
+    @Throws(IOException::class)
+    private fun getInputStream(filter: Function<InputStream, InputStream>?): InputStream? {
+        val bufStream =
+            BufferedInputStream(
+                when (source) {
+                    is UrlSource -> source.value.openStream()
+                    is PathSource -> FileInputStream(source.value.toFile())
+                }
+            )
+        return filter?.apply(bufStream) ?: 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.
+     */
+    @Throws(IOException::class)
+    fun installTo(dir: Path, filter: Function<InputStream, InputStream>?) {
+        val source =
+            when (source) {
+                is PathSource -> source.value.toString()
+                is UrlSource -> source.value.toString()
+            }
+        Log.d(TAG, "Installing. source: $source, destination: $dir")
+        TarArchiveInputStream(GzipCompressorInputStream(getInputStream(filter))).use { tarStream ->
+            Files.createDirectories(dir)
+            var entry: ArchiveEntry?
+            while ((tarStream.nextEntry.also { entry = it }) != null) {
+                val to = dir.resolve(entry!!.getName())
+                if (Files.isDirectory(to)) {
+                    Files.createDirectories(to)
+                    continue
+                }
+                Files.copy(tarStream, to, StandardCopyOption.REPLACE_EXISTING)
+            }
+        }
+        commitInstallationAt(dir)
+    }
+
+    @Throws(IOException::class)
+    private fun commitInstallationAt(dir: Path) {
+        // To save storage, delete the source archive on the disk.
+        if (source is PathSource) {
+            Files.deleteIfExists(source.value)
+        }
+
+        // Mark the completion
+        val marker = dir.resolve(InstalledImage.MARKER_FILENAME)
+        Files.createFile(marker)
+    }
+
+    companion object {
+        private const val DIR_IN_SDCARD = "linux"
+        private const val ARCHIVE_NAME = "images.tar.gz"
+        private const val BUILD_TAG = "latest" // TODO: use actual tag name
+        private const val HOST_URL = "https://dl.google.com/android/ferrochrome/$BUILD_TAG"
+
+        @JvmStatic
+        fun getSdcardPathForTesting(): Path? {
+            return Environment.getExternalStoragePublicDirectory(DIR_IN_SDCARD).toPath()
+        }
+
+        /**
+         * Creates ImageArchive which is located in the sdcard. This archive is for testing only.
+         */
+        @JvmStatic
+        fun fromSdCard(): ImageArchive {
+            return ImageArchive(getSdcardPathForTesting()!!.resolve(ARCHIVE_NAME))
+        }
+
+        /**
+         * Creates ImageArchive which is hosted in the Google server. This is the official archive.
+         */
+        @JvmStatic
+        fun fromInternet(): ImageArchive {
+            val arch =
+                if (listOf<String?>(*Build.SUPPORTED_ABIS).contains("x86_64")) "x86_64"
+                else "aarch64"
+            try {
+                return ImageArchive(URL("$HOST_URL/$arch/$ARCHIVE_NAME"))
+            } catch (e: MalformedURLException) {
+                // cannot happen
+                throw RuntimeException(e)
+            }
+        }
+
+        /**
+         * Creates ImageArchive from either SdCard or Internet. SdCard is used only when the build
+         * is debuggable and the file actually exists.
+         */
+        @JvmStatic
+        fun getDefault(): ImageArchive {
+            val archive = fromSdCard()
+            return if (Build.isDebuggable() && archive.exists()) {
+                archive
+            } else {
+                fromInternet()
+            }
+        }
+    }
+}