Add LinuxInstallerApp(and Stub)
Bug: 357827587
Bug: 362897977
Test: m && flash, and then setup.sh && m LinuxInstallerApp && adb
install
Change-Id: Ib74debb1f4cf3aaf87fd17f5ff3b6df3c342b54f
diff --git a/android/FerrochromeApp/Android.bp b/android/FerrochromeApp/Android.bp
index 9f0c735..3e4ad14 100644
--- a/android/FerrochromeApp/Android.bp
+++ b/android/FerrochromeApp/Android.bp
@@ -2,17 +2,22 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
+java_defaults {
+ name: "VmPayloadInstaller",
+ init_rc: [":custom_vm_setup.rc"],
+ required: ["custom_vm_setup"],
+ // TODO(b/348113995): move this app to product partition
+ system_ext_specific: true,
+ platform_apis: true,
+ privileged: true,
+}
+
android_app {
name: "FerrochromeApp",
srcs: ["java/**/*.java"],
resource_dirs: ["res"],
- platform_apis: true,
- // TODO(b/348113995): move this app to product partition
- system_ext_specific: true,
- privileged: true,
- init_rc: ["custom_vm_setup.rc"],
+ defaults: ["VmPayloadInstaller"],
required: [
- "custom_vm_setup",
"privapp-permissions-ferrochrome.xml",
],
}
@@ -24,6 +29,11 @@
system_ext_specific: true,
}
+filegroup {
+ name: "custom_vm_setup.rc",
+ srcs: ["custom_vm_setup.rc"],
+}
+
sh_binary {
name: "custom_vm_setup",
src: "custom_vm_setup.sh",
diff --git a/android/FerrochromeApp/custom_vm_setup.sh b/android/FerrochromeApp/custom_vm_setup.sh
index 4dce0c7..df1a3a6 100644
--- a/android/FerrochromeApp/custom_vm_setup.sh
+++ b/android/FerrochromeApp/custom_vm_setup.sh
@@ -15,12 +15,13 @@
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
-
+ 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
}
diff --git a/android/LinuxInstaller/.gitignore b/android/LinuxInstaller/.gitignore
new file mode 100644
index 0000000..e81da29
--- /dev/null
+++ b/android/LinuxInstaller/.gitignore
@@ -0,0 +1,2 @@
+assets/*
+!assets/.gitkeep
diff --git a/android/LinuxInstaller/Android.bp b/android/LinuxInstaller/Android.bp
new file mode 100644
index 0000000..5f34c63
--- /dev/null
+++ b/android/LinuxInstaller/Android.bp
@@ -0,0 +1,34 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+ name: "LinuxInstallerApp",
+ srcs: ["java/**/*.java"],
+ resource_dirs: ["res"],
+ asset_dirs: ["assets"],
+ manifest: "AndroidManifest.xml",
+ defaults: ["VmPayloadInstaller"],
+ overrides: ["LinuxInstallerAppStub"],
+ required: [
+ "privapp-permissions-linuxinstaller.xml",
+ ],
+}
+
+android_app {
+ name: "LinuxInstallerAppStub",
+ srcs: ["java/**/*.java"],
+ resource_dirs: ["res"],
+ manifest: "AndroidManifest_stub.xml",
+ defaults: ["VmPayloadInstaller"],
+ required: [
+ "privapp-permissions-linuxinstaller.xml",
+ ],
+}
+
+prebuilt_etc {
+ name: "privapp-permissions-linuxinstaller.xml",
+ src: "privapp-permissions-linuxinstaller.xml",
+ sub_dir: "permissions",
+ system_ext_specific: true,
+}
diff --git a/android/LinuxInstaller/AndroidManifest.xml b/android/LinuxInstaller/AndroidManifest.xml
new file mode 100644
index 0000000..5b10d9e
--- /dev/null
+++ b/android/LinuxInstaller/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.virtualization.linuxinstaller" >
+ <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />
+ <queries>
+ <intent>
+ <action android:name="android.virtualization.VM_TERMINAL" />
+ </intent>
+ </queries>
+ <application
+ android:label="LinuxInstaller">
+ <activity android:name=".MainActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/android/LinuxInstaller/AndroidManifest_stub.xml b/android/LinuxInstaller/AndroidManifest_stub.xml
new file mode 100644
index 0000000..49365ea
--- /dev/null
+++ b/android/LinuxInstaller/AndroidManifest_stub.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.virtualization.linuxinstaller" >
+ <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />
+ <queries>
+ <intent>
+ <action android:name="android.virtualization.VM_TERMINAL" />
+ </intent>
+ </queries>
+ <application
+ android:label="LinuxInstaller">
+ <activity android:name=".MainActivity"
+ android:exported="true">
+ </activity>
+ </application>
+
+</manifest>
diff --git a/android/LinuxInstaller/assets/.gitkeep b/android/LinuxInstaller/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/android/LinuxInstaller/assets/.gitkeep
diff --git a/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java b/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java
new file mode 100644
index 0000000..1d875cb
--- /dev/null
+++ b/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java
@@ -0,0 +1,207 @@
+/*
+ * 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.linuxinstaller;
+
+import android.annotation.WorkerThread;
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+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.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class MainActivity extends Activity {
+ private static final String TAG = "LinuxInstaller";
+ private static final String ACTION_VM_TERMINAL = "android.virtualization.VM_TERMINAL";
+
+ 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);
+ setContentView(R.layout.activity_main);
+
+ executorService.execute(this::installLinuxImage);
+ }
+
+ private void installLinuxImage() {
+ ComponentName vmTerminalComponent = resolve(getPackageManager(), ACTION_VM_TERMINAL);
+ if (vmTerminalComponent == null) {
+ updateStatus("Failed to resolve VM terminal");
+ return;
+ }
+
+ if (!hasLocalAssets()) {
+ updateStatus("No local assets");
+ return;
+ }
+ try {
+ updateImageIfNeeded();
+ } catch (IOException e) {
+ Log.e(TAG, "failed to update image", e);
+ return;
+ }
+ updateStatus("Enabling terminal app...");
+ getPackageManager()
+ .setComponentEnabledSetting(
+ vmTerminalComponent,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP);
+ updateStatus("Done.");
+ }
+
+ @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.custom_vm_setup.path", destDir);
+ SystemProperties.set("debug.custom_vm_setup.done", "false");
+ SystemProperties.set("debug.custom_vm_setup.start", "true");
+ while (!SystemProperties.getBoolean("debug.custom_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");
+ });
+ }
+
+ private ComponentName resolve(PackageManager pm, String action) {
+ Intent intent = new Intent(action);
+ List<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, PackageManager.MATCH_ALL);
+ if (resolveInfos.size() != 1) {
+ Log.w(
+ TAG,
+ "Failed to resolve activity, action=" + action + ", resolved=" + resolveInfos);
+ return null;
+ }
+ ActivityInfo activityInfo = resolveInfos.getFirst().activityInfo;
+ // MainActivityAlias shows in Launcher
+ return new ComponentName(activityInfo.packageName, activityInfo.name + "Alias");
+ }
+}
diff --git a/android/LinuxInstaller/linux_image_builder/.gitignore b/android/LinuxInstaller/linux_image_builder/.gitignore
deleted file mode 100644
index f082413..0000000
--- a/android/LinuxInstaller/linux_image_builder/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-debian.img
-ttyd
diff --git a/android/LinuxInstaller/linux_image_builder/commands b/android/LinuxInstaller/linux_image_builder/commands
index 8b2da45..4d27475 100644
--- a/android/LinuxInstaller/linux_image_builder/commands
+++ b/android/LinuxInstaller/linux_image_builder/commands
@@ -1,6 +1,6 @@
upload init.sh:/root
upload vsock.py:/usr/local/bin
-upload ttyd:/usr/local/bin
+upload /tmp/ttyd:/usr/local/bin
upload ttyd.service:/etc/systemd/system
upload vsockip.service:/etc/systemd/system
chmod 0777:/root/init.sh
diff --git a/android/LinuxInstaller/linux_image_builder/setup.sh b/android/LinuxInstaller/linux_image_builder/setup.sh
index a9aa77d..2883e61 100755
--- a/android/LinuxInstaller/linux_image_builder/setup.sh
+++ b/android/LinuxInstaller/linux_image_builder/setup.sh
@@ -1,4 +1,29 @@
#!/bin/bash
-wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-nocloud-arm64.raw -O debian.img
-wget https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.aarch64 -O ttyd
-virt-customize --commands-from-file commands -a debian.img
\ No newline at end of file
+
+pushd $(dirname $0) > /dev/null
+tempdir=$(mktemp -d)
+echo Get Debian image and dependencies...
+wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-nocloud-arm64.raw -O ${tempdir}/debian.img
+wget https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.aarch64 -O ${tempdir}/ttyd
+
+echo Customize the image...
+virt-customize --commands-from-file <(sed "s|/tmp|$tempdir|g" commands) -a ${tempdir}/debian.img
+
+asset_dir=../assets/linux
+mkdir -p ${asset_dir}
+
+echo Copy files...
+
+pushd ${tempdir} > /dev/null
+tar czvS -f images.tar.gz debian.img
+popd > /dev/null
+mv ${tempdir}/images.tar.gz ${asset_dir}/images.tar.gz
+cp vm_config.json ${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}
\ No newline at end of file
diff --git a/android/LinuxInstaller/linux_image_builder/vm_config.json b/android/LinuxInstaller/linux_image_builder/vm_config.json
new file mode 100644
index 0000000..21462b8
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/vm_config.json
@@ -0,0 +1,19 @@
+{
+ "name": "debian",
+ "disks": [
+ {
+ "image": "/data/local/tmp/debian.img",
+ "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
+}
diff --git a/android/LinuxInstaller/privapp-permissions-linuxinstaller.xml b/android/LinuxInstaller/privapp-permissions-linuxinstaller.xml
new file mode 100644
index 0000000..e46ec97
--- /dev/null
+++ b/android/LinuxInstaller/privapp-permissions-linuxinstaller.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<permissions>
+ <privapp-permissions package="com.android.virtualization.linuxinstaller">
+ <permission name="android.permission.CHANGE_COMPONENT_ENABLED_STATE"/>
+ </privapp-permissions>
+</permissions>
\ No newline at end of file
diff --git a/android/LinuxInstaller/res/layout/activity_main.xml b/android/LinuxInstaller/res/layout/activity_main.xml
new file mode 100644
index 0000000..3967167
--- /dev/null
+++ b/android/LinuxInstaller/res/layout/activity_main.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/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index 27b2b46..ed9527c 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -11,13 +11,22 @@
<activity android:name=".MainActivity"
android:screenOrientation="landscape"
android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
- android:exported="true"
- android:enabled="false">
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.virtualization.VM_TERMINAL" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <activity-alias
+ android:name=".MainActivityAlias"
+ android:targetActivity="com.android.virtualization.terminal.MainActivity"
+ android:exported="true"
+ android:enabled="false" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
- </activity>
+ </activity-alias>
</application>
</manifest>