Put ferrochrome image into the app

This CL includes the follwing changes:

* The ferrochrome image is included in the app as an asset. It's not
  downloaded.

* The image is a compressed archive having four partition images. The
  archive was created as follows:

   - chromium_base_image.bin is downloaded
   - all 12 partitions are extracted using the unpack_partitions.sh
     script
   - four partitions (efi, root, kernel, state) are archieved using tar
     with the --sparse option

* At runtime, the archive is extracted to /data/local/tmp and the copy
  of the archive in /data/media/<user_id>/ferrochrome is deleted to save
  space.

* The VM config now specifis a composite disk where partitions are from
  the extracted partition images.

* The state.img is rounded up to the multiple of 4K as required by
  crosvm.

Note: this change does NOT include the ferrochrome image. It will be
uploaded to the internal gerrit.

Bug: 351973725
Bug: 351904150
Test: run ferrochrome app
Change-Id: I12a6a1efa270065780a1cbdc61a3edd92f4f28aa
diff --git a/ferrochrome_app/custom_vm_setup.sh b/ferrochrome_app/custom_vm_setup.sh
index f007f6a..a5480ff 100644
--- a/ferrochrome_app/custom_vm_setup.sh
+++ b/ferrochrome_app/custom_vm_setup.sh
@@ -1,14 +1,31 @@
 #!/system/bin/sh
 
-function copy_files() {
-  cp -u /sdcard/vm_config.json /data/local/tmp
-  cp -u /data/media/10/vm_config.json /data/local/tmp
-  cp -u /sdcard/chromiumos_test_image.bin /data/local/tmp
-  cp -u /data/media/10/chromiumos_test_image.bin /data/local/tmp
-  chmod 666 /data/local/tmp/vm_config.json
-  chmod 666 /data/local/tmp/chromiumos_test_image.bin
+function round_up() {
+  num=$1
+  div=$2
+  echo $((( (( ${num} / ${div} ) + 1) * ${div} )))
 }
+
+function install() {
+  user=$(cmd user get-main-user)
+  src_dir=/data/media/${user}/ferrochrome/
+  dst_dir=/data/local/tmp/
+
+  cat $(find ${src_dir} -name "images.tar.gz*" | sort) | tar xz -C ${dst_dir}
+  cp -u ${src_dir}vm_config.json ${dst_dir}
+  chmod 666 ${dst_dir}*
+
+  # increase the size of state.img to the multiple of 4096
+  num_blocks=$(du -b -K ${dst_dir}state.img | cut -f 1)
+  required_num_blocks=$(round_up ${num_blocks} 4)
+  additional_blocks=$((( ${required_num_blocks} - ${num_blocks} )))
+  dd if=/dev/zero bs=512 count=${additional_blocks} >> ${dst_dir}state.img
+
+  rm ${src_dir}images.tar.gz*
+  rm ${src_dir}vm_config.json
+}
+
 setprop debug.custom_vm_setup.done false
-copy_files
+install
 setprop debug.custom_vm_setup.start false
 setprop debug.custom_vm_setup.done true
diff --git a/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
index 7c18537..58005aa 100644
--- a/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
+++ b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
@@ -28,15 +28,11 @@
 import android.view.WindowManager;
 import android.widget.TextView;
 
-import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
-import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
-import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
-
-import java.io.File;
+import java.io.BufferedReader;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
+import java.io.InputStreamReader;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
@@ -45,19 +41,17 @@
 import java.util.concurrent.Executors;
 
 public class FerrochromeActivity extends Activity {
-    ExecutorService executorService = Executors.newSingleThreadExecutor();
-    private static final String TAG = "FerrochromeActivity";
+    private static final String TAG = FerrochromeActivity.class.getName();
     private static final String ACTION_VM_LAUNCHER = "android.virtualization.VM_LAUNCHER";
-    private static final String FERROCHROME_VERSION = "R128-15926.0.0";
-    private static final String EXTERNAL_STORAGE_DIR =
-            Environment.getExternalStorageDirectory().getPath() + File.separator;
-    private static final Path IMAGE_PATH =
-            Path.of(EXTERNAL_STORAGE_DIR + "chromiumos_test_image.bin");
-    private static final Path IMAGE_VERSION_INFO =
-            Path.of(EXTERNAL_STORAGE_DIR + "ferrochrome_image_version");
-    private static final Path VM_CONFIG_PATH = Path.of(EXTERNAL_STORAGE_DIR + "vm_config.json");
+
+    private static final Path DEST_DIR =
+            Path.of(Environment.getExternalStorageDirectory().getPath(), "ferrochrome");
+    private static final Path VERSION_FILE = Path.of(DEST_DIR.toString(), "version");
+
     private static final int REQUEST_CODE_VMLAUNCHER = 1;
 
+    ExecutorService executorService = Executors.newSingleThreadExecutor();
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -80,35 +74,7 @@
 
         executorService.execute(
                 () -> {
-                    if (Files.notExists(IMAGE_PATH)
-                            || !FERROCHROME_VERSION.equals(getVersionInfo())) {
-                        updateStatus("Starting first-time setup.");
-                        updateStatus(
-                                "Downloading Ferrochrome image. This can take about 5 to 10"
-                                        + " minutes, depending on your network speed.");
-                        if (download(FERROCHROME_VERSION)) {
-                            updateStatus("Done.");
-                        } else {
-                            updateStatus(
-                                    "Download failed. Check the internet connection and retry.");
-                            return;
-                        }
-                    } else {
-                        updateStatus("Ferrochrome is already downloaded.");
-                    }
-                    updateStatus("Updating VM config.");
-                    copyVmConfigJson();
-                    updateStatus("Updating VM images. This may take a few minutes.");
-                    SystemProperties.set("debug.custom_vm_setup.start", "true");
-                    while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
-                        // Wait for custom_vm_setup
-                        try {
-                            Thread.sleep(1000);
-                        } catch (Exception e) {
-                            Log.d(TAG, e.toString());
-                        }
-                    }
-                    updateStatus("Done.");
+                    updateImageIfNeeded();
                     updateStatus("Starting Ferrochrome...");
                     runOnUiThread(() -> startActivityForResult(intent, REQUEST_CODE_VMLAUNCHER));
                 });
@@ -121,63 +87,86 @@
         }
     }
 
+    private void updateImageIfNeeded() {
+        if (!isUpdateNeeded()) {
+            Log.d(TAG, "No update needed.");
+            return;
+        }
+
+        updateStatus("Copying images...");
+        try {
+            if (Files.notExists(DEST_DIR)) {
+                Files.createDirectory(DEST_DIR);
+            }
+            for (String file : getAssets().list("ferrochrome")) {
+                updateStatus(file);
+                Path dst = Path.of(DEST_DIR.toString(), file);
+                updateFile(getAssets().open("ferrochrome/" + file), dst);
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Error while updating image: " + e);
+            updateStatus("Failed.");
+            return;
+        }
+        updateStatus("Done.");
+
+        updateStatus("Extracting images...");
+        SystemProperties.set("debug.custom_vm_setup.start", "true");
+        while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
+            try {
+                Thread.sleep(1000);
+            } catch (Exception e) {
+                Log.e(TAG, "Error while extracting image: " + e);
+                updateStatus("Failed.");
+                return;
+            }
+        }
+        updateStatus("Done.");
+    }
+
+    private boolean isUpdateNeeded() {
+        Path[] pathsToCheck = {DEST_DIR, VERSION_FILE};
+        for (Path p : pathsToCheck) {
+            if (Files.notExists(p)) {
+                Log.d(TAG, p.toString() + " does not exist.");
+                return true;
+            }
+        }
+
+        try {
+            String installedVer = readLine(new FileInputStream(VERSION_FILE.toFile()));
+            String updatedVer = readLine(getAssets().open("ferrochrome/version"));
+            if (installedVer.equals(updatedVer)) {
+                return false;
+            }
+            Log.d(TAG, "Version mismatch. Installed: " + installedVer + "  Updated: " + updatedVer);
+        } catch (IOException e) {
+            Log.e(TAG, "Error while checking version: " + e);
+        }
+        return true;
+    }
+
+    private static String readLine(InputStream input) throws IOException {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(input))) {
+            return reader.readLine();
+        } catch (IOException e) {
+            throw e;
+        }
+    }
+
+    private static void updateFile(InputStream input, Path path) throws IOException {
+        try {
+            Files.copy(input, path, StandardCopyOption.REPLACE_EXISTING);
+        } finally {
+            input.close();
+        }
+    }
+
     private void updateStatus(String line) {
-        Log.d(TAG, line);
         runOnUiThread(
                 () -> {
                     TextView statusView = findViewById(R.id.status_txt_view);
                     statusView.append(line + "\n");
                 });
     }
-
-    private void copyVmConfigJson() {
-        try (InputStream is = getResources().openRawResource(R.raw.vm_config)) {
-            Files.copy(is, VM_CONFIG_PATH, StandardCopyOption.REPLACE_EXISTING);
-        } catch (IOException e) {
-            updateStatus(e.toString());
-        }
-    }
-
-    private String getVersionInfo() {
-        try {
-            return new String(Files.readAllBytes(IMAGE_VERSION_INFO), StandardCharsets.UTF_8);
-        } catch (IOException e) {
-            return null;
-        }
-    }
-
-    private boolean updateVersionInfo(String version) {
-        try {
-            Files.write(IMAGE_VERSION_INFO, version.getBytes(StandardCharsets.UTF_8));
-        } catch (IOException e) {
-            Log.d(TAG, e.toString());
-        }
-        return true;
-    }
-
-    private boolean download(String version) {
-        String urlString =
-                "https://storage.googleapis.com/chromiumos-image-archive/ferrochrome-public/"
-                        + version
-                        + "/chromiumos_test_image.tar.xz";
-        try (InputStream is = (new URL(urlString)).openStream();
-                XZCompressorInputStream xz = new XZCompressorInputStream(is);
-                TarArchiveInputStream tar = new TarArchiveInputStream(xz)) {
-            TarArchiveEntry entry;
-            while ((entry = tar.getNextTarEntry()) != null) {
-                if (!entry.getName().contains("chromiumos_test_image.bin")) {
-                    continue;
-                }
-                updateStatus("copy " + entry.getName() + " start");
-                Files.copy(tar, IMAGE_PATH, StandardCopyOption.REPLACE_EXISTING);
-                updateStatus("copy " + entry.getName() + " done");
-                updateVersionInfo(version);
-                break;
-            }
-        } catch (Exception e) {
-            updateStatus(e.toString());
-            return false;
-        }
-        return true;
-    }
 }
diff --git a/ferrochrome_app/res/raw/vm_config.json b/ferrochrome_app/res/raw/vm_config.json
deleted file mode 100644
index d79400c..0000000
--- a/ferrochrome_app/res/raw/vm_config.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
-    "name": "cros",
-    "disks": [
-        {
-            "image": "/data/local/tmp/chromiumos_test_image.bin",
-            "partitions": [],
-            "writable": true
-        }
-    ],
-    "params": "root=/dev/vda3 rootwait noinitrd ro enforcing=0 cros_debug cros_secure",
-    "protected": false,
-    "cpu_topology": "match_host",
-    "platform_version": "~1.0",
-    "memory_mib": 8096,
-    "gpu": {
-        "backend": "virglrenderer",
-        "context_types": ["virgl2"]
-    },
-    "console_input_device": "ttyS0"
-}
\ No newline at end of file