VmTerminalApp: Merge with LinuxInstaller
This CL includes following changes:
- Move linux_vm_setup
- Move LinuxInstaller's MainActivity as InstallerActivity
- InstallerActivity finishes self when done
- Remove LinuxInstaller and clean up relevant codes
Bug: 369746567
Test: manually
Change-Id: I2c97e07b00e110aebe09db2760b26257ace4f7d9
diff --git a/android/TerminalApp/.gitignore b/android/TerminalApp/.gitignore
new file mode 100644
index 0000000..e81da29
--- /dev/null
+++ b/android/TerminalApp/.gitignore
@@ -0,0 +1,2 @@
+assets/*
+!assets/.gitkeep
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index 932ca76..e5e8b0a 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -26,3 +26,15 @@
"com.android.virt",
],
}
+
+filegroup {
+ name: "linux_vm_setup.rc",
+ srcs: ["linux_vm_setup.rc"],
+}
+
+sh_binary {
+ name: "linux_vm_setup",
+ src: "linux_vm_setup.sh",
+ init_rc: [":linux_vm_setup.rc"],
+ host_supported: false,
+}
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index bd1395a..105e454 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.android.virtualization.terminal">
+ <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
<uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
<uses-permission android:name="android.permission.INTERNET" />
@@ -47,6 +48,12 @@
android:name="${applicationId}.SplitInitializer"
android:value="androidx.startup" />
</provider>
+ <activity android:name=".InstallerActivity"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
<service
android:name="com.android.virtualization.vmlauncher.VmLauncherService"
diff --git a/android/TerminalApp/assets/.gitkeep b/android/TerminalApp/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/android/TerminalApp/assets/.gitkeep
diff --git a/android/TerminalApp/generate_assets.sh b/android/TerminalApp/generate_assets.sh
new file mode 100755
index 0000000..ff7444e
--- /dev/null
+++ b/android/TerminalApp/generate_assets.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+set -e
+
+if [ "$#" -ne 1 ]; then
+ echo "$0 <image.raw path>"
+ echo "image.raw can be built with packages/modules/Virtualization/build/debian/build.sh"
+ exit 1
+fi
+pushd $(dirname $0) > /dev/null
+tempdir=$(mktemp -d)
+asset_dir=./assets/linux
+mkdir -p ${asset_dir}
+echo Copy files...
+pushd ${tempdir} > /dev/null
+cp "$1" ${tempdir}
+tar czvS -f images.tar.gz $(basename $1)
+popd > /dev/null
+cp vm_config.json ${asset_dir}
+mv ${tempdir}/images.tar.gz ${asset_dir}
+echo Calculating hash...
+hash=$(cat ${asset_dir}/images.tar.gz ${asset_dir}/vm_config.json | sha1sum | cut -d' ' -f 1)
+echo ${hash} > ${asset_dir}/hash
+popd > /dev/null
+echo Cleaning up...
+rm -rf ${tempdir}
+
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
new file mode 100644
index 0000000..1c739e2
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 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.annotation.WorkerThread;
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.TextView;
+
+import libcore.io.Streams;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class InstallerActivity extends Activity {
+ private static final String TAG = "LinuxInstaller";
+
+ private static final Path DEST_DIR =
+ Path.of(Environment.getExternalStorageDirectory().getPath(), "linux");
+
+ private static final String ASSET_DIR = "linux";
+ private static final String HASH_FILE_NAME = "hash";
+ private static final Path HASH_FILE = Path.of(DEST_DIR.toString(), HASH_FILE_NAME);
+
+ ExecutorService executorService = Executors.newSingleThreadExecutor();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setResult(RESULT_CANCELED);
+
+ setContentView(R.layout.activity_installer);
+
+ executorService.execute(this::installLinuxImage);
+ }
+
+ private void installLinuxImage() {
+ if (!hasLocalAssets()) {
+ updateStatus("No local assets");
+ setResult(RESULT_CANCELED, null);
+ return;
+ }
+ try {
+ updateImageIfNeeded();
+ } catch (IOException e) {
+ Log.e(TAG, "failed to update image", e);
+ return;
+ }
+ updateStatus("Done.");
+ setResult(RESULT_OK);
+ finish();
+ }
+
+ @WorkerThread
+ private boolean hasLocalAssets() {
+ try {
+ String[] files = getAssets().list(ASSET_DIR);
+ return files != null && files.length > 0;
+ } catch (IOException e) {
+ Log.e(TAG, "there is an error during listing up assets", e);
+ return false;
+ }
+ }
+
+ @WorkerThread
+ private void updateImageIfNeeded() throws IOException {
+ if (!isUpdateNeeded()) {
+ Log.d(TAG, "No update needed.");
+ return;
+ }
+
+ try {
+ if (Files.notExists(DEST_DIR)) {
+ Files.createDirectory(DEST_DIR);
+ }
+
+ updateStatus("Copying images...");
+ String[] files = getAssets().list(ASSET_DIR);
+ for (String file : files) {
+ updateStatus(file);
+ Path dst = Path.of(DEST_DIR.toString(), file);
+ updateFile(getAssets().open(ASSET_DIR + "/" + file), dst);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Error while updating image: " + e);
+ updateStatus("Failed to update image.");
+ throw e;
+ }
+ extractImages(DEST_DIR.toAbsolutePath().toString());
+ }
+
+ @WorkerThread
+ private void extractImages(String destDir) throws IOException {
+ updateStatus("Extracting images...");
+
+ if (TextUtils.isEmpty(destDir)) {
+ throw new RuntimeException("Internal error: destDir shouldn't be null");
+ }
+
+ SystemProperties.set("debug.linux_vm_setup.path", destDir);
+ SystemProperties.set("debug.linux_vm_setup.done", "false");
+ SystemProperties.set("debug.linux_vm_setup.start", "true");
+ while (!SystemProperties.getBoolean("debug.linux_vm_setup.done", false)) {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Error while extracting image: " + e);
+ updateStatus("Failed to extract image.");
+ throw new IOException("extracting image is interrupted", e);
+ }
+ }
+ }
+
+ @WorkerThread
+ private boolean isUpdateNeeded() {
+ Path[] pathsToCheck = {DEST_DIR, HASH_FILE};
+ for (Path p : pathsToCheck) {
+ if (Files.notExists(p)) {
+ Log.d(TAG, p.toString() + " does not exist.");
+ return true;
+ }
+ }
+
+ try {
+ String installedHash = readAll(new FileInputStream(HASH_FILE.toFile()));
+ String updatedHash = readAll(getAssets().open(ASSET_DIR + "/" + HASH_FILE_NAME));
+ if (installedHash.equals(updatedHash)) {
+ return false;
+ }
+ Log.d(TAG, "Hash mismatch. Installed: " + installedHash + " Updated: " + updatedHash);
+ } catch (IOException e) {
+ Log.e(TAG, "Error while checking hash: " + e);
+ }
+ return true;
+ }
+
+ private static String readAll(InputStream input) throws IOException {
+ return Streams.readFully(new InputStreamReader(input)).strip();
+ }
+
+ private static void updateFile(InputStream input, Path path) throws IOException {
+ try (input) {
+ Files.copy(input, path, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+
+ private void updateStatus(String line) {
+ runOnUiThread(
+ () -> {
+ TextView statusView = findViewById(R.id.status_txt_view);
+ statusView.append(line + "\n");
+ });
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index 3aca929..bd35f51 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -66,6 +66,8 @@
private static final String TAG = "VmTerminalApp";
private static final String VM_ADDR = "192.168.0.2";
private static final int TTYD_PORT = 7681;
+ private static final int REQUEST_CODE_INSTALLER = 0x33;
+
private X509Certificate[] mCertificates;
private PrivateKey mPrivateKey;
private WebView mWebView;
@@ -75,6 +77,7 @@
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ checkForUpdate();
try {
// No resize for now.
long newSizeInBytes = 0;
@@ -85,10 +88,6 @@
.show();
}
- Toast.makeText(this, R.string.vm_creation_message, Toast.LENGTH_SHORT).show();
- android.os.Trace.beginAsyncSection("executeTerminal", 0);
- VmLauncherServices.startVmLauncherService(this, this);
-
setContentView(R.layout.activity_headless);
MaterialToolbar toolbar = (MaterialToolbar) findViewById(R.id.toolbar);
@@ -369,4 +368,28 @@
public void onTouchExplorationStateChanged(boolean enabled) {
connectToTerminalService();
}
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == REQUEST_CODE_INSTALLER) {
+ if (resultCode != RESULT_OK) {
+ Log.e(TAG, "Failed to start VM. Installer returned error.");
+ finish();
+ }
+ startVm();
+ }
+ }
+
+ private void checkForUpdate() {
+ Intent intent = new Intent(this, InstallerActivity.class);
+ startActivityForResult(intent, REQUEST_CODE_INSTALLER);
+ }
+
+ private void startVm() {
+ Toast.makeText(this, R.string.vm_creation_message, Toast.LENGTH_SHORT).show();
+ android.os.Trace.beginAsyncSection("executeTerminal", 0);
+ VmLauncherServices.startVmLauncherService(this, this);
+ }
}
diff --git a/android/TerminalApp/linux_vm_setup.rc b/android/TerminalApp/linux_vm_setup.rc
new file mode 100644
index 0000000..ac91532
--- /dev/null
+++ b/android/TerminalApp/linux_vm_setup.rc
@@ -0,0 +1,23 @@
+# Copyright 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.
+
+service linux_vm_setup /system/bin/linux_vm_setup
+ user shell
+ group shell media_rw
+ disabled
+ oneshot
+ seclabel u:r:shell:s0
+
+on property:debug.linux_vm_setup.start=true
+ start linux_vm_setup
diff --git a/android/TerminalApp/linux_vm_setup.sh b/android/TerminalApp/linux_vm_setup.sh
new file mode 100644
index 0000000..6a93f6f
--- /dev/null
+++ b/android/TerminalApp/linux_vm_setup.sh
@@ -0,0 +1,32 @@
+#!/system/bin/sh
+
+function round_up() {
+ num=$1
+ div=$2
+ echo $((( (( ${num} / ${div} ) + 1) * ${div} )))
+}
+
+function install() {
+ src_dir=$(getprop debug.linux_vm_setup.path)
+ src_dir=${src_dir/#\/storage\/emulated\//\/data\/media\/}
+ 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}/*
+
+ if [ -f ${dst_dir}state.img ]; then
+ # 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
+ fi
+ rm ${src_dir}/images.tar.gz*
+ rm ${src_dir}/vm_config.json
+}
+
+setprop debug.linux_vm_setup.done false
+install
+setprop debug.linux_vm_setup.start false
+setprop debug.linux_vm_setup.done true
diff --git a/android/TerminalApp/res/layout/activity_installer.xml b/android/TerminalApp/res/layout/activity_installer.xml
new file mode 100644
index 0000000..3967167
--- /dev/null
+++ b/android/TerminalApp/res/layout/activity_installer.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp">
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="28sp"
+ android:id="@+id/status_txt_view"/>
+
+</RelativeLayout>
diff --git a/android/TerminalApp/vm_config.json b/android/TerminalApp/vm_config.json
new file mode 100644
index 0000000..474e9c3
--- /dev/null
+++ b/android/TerminalApp/vm_config.json
@@ -0,0 +1,21 @@
+
+{
+ "name": "debian",
+ "disks": [
+ {
+ "image": "/data/local/tmp/image.raw",
+ "partitions": [],
+ "writable": true
+ }
+ ],
+ "protected": false,
+ "cpu_topology": "match_host",
+ "platform_version": "~1.0",
+ "memory_mib": 4096,
+ "debuggable": true,
+ "console_out": true,
+ "connect_console": true,
+ "console_input_device": "ttyS0",
+ "network": true
+}
+