Merge "(experimental) Enable VirGL if the flag file exists" 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 cec1b7a..5cf123e 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java
@@ -38,14 +38,13 @@
 import java.io.BufferedReader;
 import java.io.FileReader;
 import java.io.IOException;
-import java.io.PipedReader;
-import java.io.PipedWriter;
 import java.io.Reader;
 import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
+import java.util.stream.Collectors;
 
 /** This class and its inner classes model vm_config.json. */
 class ConfigJson {
@@ -78,19 +77,15 @@
 
     /** Parses JSON file at jsonPath */
     static ConfigJson from(Context context, Path jsonPath) {
-        try (FileReader fileReader = new FileReader(jsonPath.toFile());
-                Reader r = replaceKeywords(fileReader, context)) {
-            return new Gson().fromJson(r, ConfigJson.class);
+        try (FileReader fileReader = new FileReader(jsonPath.toFile())) {
+            String content = replaceKeywords(fileReader, context);
+            return new Gson().fromJson(content, ConfigJson.class);
         } catch (Exception e) {
             throw new RuntimeException("Failed to parse " + jsonPath, e);
         }
     }
 
-    private static Reader replaceKeywords(Reader r, Context context) throws IOException {
-        PipedWriter pipeIn = new PipedWriter();
-        PipedReader pipeOut = new PipedReader();
-        pipeOut.connect(pipeIn);
-
+    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("\\$USER_ID", String.valueOf(context.getUserId()));
@@ -103,7 +98,7 @@
         rules.put("\\$APP_DATA_DIR", appDataDir);
 
         try (BufferedReader br = new BufferedReader(r)) {
-            br.lines()
+            return br.lines()
                     .map(
                             line -> {
                                 for (Map.Entry<String, String> rule : rules.entrySet()) {
@@ -111,18 +106,8 @@
                                 }
                                 return line;
                             })
-                    .forEach(
-                            line -> {
-                                try {
-                                    pipeIn.write(line);
-                                    pipeIn.write('\n');
-                                } catch (IOException e) {
-                                    // this cannot happen as it is connected to a pipe.
-                                    throw new RuntimeException(e);
-                                }
-                            });
+                    .collect(Collectors.joining("\n"));
         }
-        return pipeOut;
     }
 
     private int getCpuTopology() {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ErrorActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/ErrorActivity.java
index ee1f1ad..44dcce5 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ErrorActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ErrorActivity.java
@@ -16,6 +16,7 @@
 
 package com.android.virtualization.terminal;
 
+import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.View;
@@ -25,7 +26,14 @@
 import androidx.annotation.Nullable;
 
 public class ErrorActivity extends BaseActivity {
-    public static final String EXTRA_CAUSE = "cause";
+    private static final String EXTRA_CAUSE = "cause";
+
+    public static void start(Context context, Exception e) {
+        Intent intent = new Intent(context, ErrorActivity.class);
+        intent.putExtra(EXTRA_CAUSE, e);
+        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+        context.startActivity(intent);
+    }
 
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
index c4d11e8..54aa07a 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
@@ -27,6 +27,7 @@
 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;
@@ -80,6 +81,51 @@
         }
     }
 
+    /**
+     * 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();
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
index 52ef3d4..69b5ee7 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
@@ -41,6 +41,7 @@
 import com.google.android.material.progressindicator.LinearProgressIndicator;
 import com.google.android.material.snackbar.Snackbar;
 
+import java.io.IOException;
 import java.lang.ref.WeakReference;
 
 public class InstallerActivity extends BaseActivity {
@@ -63,12 +64,8 @@
         mInstallProgressListener = new InstallProgressListener(this);
 
         setContentView(R.layout.activity_installer);
-
-        TextView desc = (TextView) findViewById(R.id.installer_desc);
-        desc.setText(
-                getString(
-                        R.string.installer_desc_text_format,
-                        Formatter.formatShortFileSize(this, ESTIMATED_IMG_SIZE_BYTES)));
+        updateSizeEstimation(ESTIMATED_IMG_SIZE_BYTES);
+        measureImageSizeAndUpdateDescription();
 
         mWaitForWifiCheckbox = (CheckBox) findViewById(R.id.installer_wait_for_wifi_checkbox);
         mInstallButton = (TextView) findViewById(R.id.installer_install_button);
@@ -85,6 +82,33 @@
         }
     }
 
+    private void updateSizeEstimation(long est) {
+        String desc =
+                getString(
+                        R.string.installer_desc_text_format,
+                        Formatter.formatShortFileSize(this, est));
+        runOnUiThread(
+                () -> {
+                    TextView view = (TextView) findViewById(R.id.installer_desc);
+                    view.setText(desc);
+                });
+    }
+
+    private void measureImageSizeAndUpdateDescription() {
+        new Thread(
+                        () -> {
+                            long est;
+                            try {
+                                est = ImageArchive.getDefault().getSize();
+                            } catch (IOException e) {
+                                Log.w(TAG, "Failed to measure image size.", e);
+                                return;
+                            }
+                            updateSizeEstimation(est);
+                        })
+                .start();
+    }
+
     @Override
     public void onResume() {
         super.onResume();
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index a3b0577..ded186f 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -197,7 +197,7 @@
     public boolean dispatchKeyEvent(KeyEvent event) {
         if (Build.isDebuggable() && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
             if (event.getAction() == KeyEvent.ACTION_UP) {
-                launchErrorActivity(new Exception("Debug: KeyEvent.KEYCODE_UNKNOWN"));
+                ErrorActivity.start(this, new Exception("Debug: KeyEvent.KEYCODE_UNKNOWN"));
             }
             return true;
         }
@@ -444,7 +444,8 @@
     @Override
     public void onVmError() {
         Log.i(TAG, "onVmError()");
-        launchErrorActivity(new Exception("onVmError"));
+        // TODO: error cause is too simple.
+        ErrorActivity.start(this, new Exception("onVmError"));
     }
 
     @Override
@@ -501,13 +502,6 @@
         }
     }
 
-    private void launchErrorActivity(Exception e) {
-        Intent intent = new Intent(this, ErrorActivity.class);
-        intent.putExtra(ErrorActivity.EXTRA_CAUSE, e);
-        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
-        this.startActivity(intent);
-    }
-
     private boolean installIfNecessary() {
         // If payload from external storage exists(only for debuggable build) or there is no
         // installed image, launch installer activity.