Merge "Some UI change to the terminal app" into main
diff --git a/android/TerminalApp/generate_assets.sh b/android/TerminalApp/generate_assets.sh
index ff7444e..4001bfd 100755
--- a/android/TerminalApp/generate_assets.sh
+++ b/android/TerminalApp/generate_assets.sh
@@ -6,14 +6,15 @@
     echo "image.raw can be built with packages/modules/Virtualization/build/debian/build.sh"
     exit 1
 fi
+image_raw_path=$(realpath $1)
 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)
+cp "${image_raw_path}" ${tempdir}
+tar czvS -f images.tar.gz $(basename ${image_raw_path})
 popd > /dev/null
 cp vm_config.json ${asset_dir}
 mv ${tempdir}/images.tar.gz ${asset_dir}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
index 4be291f..1b14ef2 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
@@ -16,27 +16,49 @@
 package com.android.virtualization.terminal
 
 import android.os.Bundle
+import android.os.FileUtils
 import android.widget.TextView
 import android.widget.Toast
+import android.text.style.RelativeSizeSpan
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.format.Formatter
+import android.text.TextUtils
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.view.isVisible
 import com.google.android.material.button.MaterialButton
 import com.google.android.material.slider.Slider
 
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
 class SettingsDiskResizeActivity : AppCompatActivity() {
+    private val maxDiskSize: Float = 256F
+    private val numberPattern: Pattern = Pattern.compile("[\\d]*[\\٫.,]?[\\d]+");
     private var diskSize: Float = 104F
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.settings_disk_resize)
-        val diskSizeText = findViewById<TextView>(R.id.settings_disk_resize_disk_size)
+        val diskSizeText = findViewById<TextView>(R.id.settings_disk_resize_resize_gb_assigned)
+        val diskMaxSizeText = findViewById<TextView>(R.id.settings_disk_resize_resize_gb_max)
+        diskMaxSizeText.text = getString(R.string.settings_disk_resize_resize_gb_max_format,
+            localizedFileSize(maxDiskSize));
+
         val diskSizeSlider = findViewById<Slider>(R.id.settings_disk_resize_disk_size_slider)
+        diskSizeSlider.setValueTo(maxDiskSize)
         val cancelButton = findViewById<MaterialButton>(R.id.settings_disk_resize_cancel_button)
         val resizeButton = findViewById<MaterialButton>(R.id.settings_disk_resize_resize_button)
-        diskSizeText.text = diskSize.toInt().toString()
         diskSizeSlider.value = diskSize
+        diskSizeText.text = enlargeFontOfNumber(
+            getString(R.string.settings_disk_resize_resize_gb_assigned_format,
+            localizedFileSize(diskSize)))
 
         diskSizeSlider.addOnChangeListener { _, value, _ ->
-            diskSizeText.text = value.toInt().toString()
+            diskSizeText.text = enlargeFontOfNumber(
+                getString(R.string.settings_disk_resize_resize_gb_assigned_format,
+                localizedFileSize(value)))
             cancelButton.isVisible = true
             resizeButton.isVisible = true
         }
@@ -54,4 +76,29 @@
                 .show()
         }
     }
+
+    fun localizedFileSize(sizeGb: Float): String {
+        // formatShortFileSize() uses SI unit (i.e. kB = 1000 bytes),
+        // so covert sizeGb with "GB" instead of "GIB".
+        val bytes = FileUtils.parseSize(sizeGb.toLong().toString() + "GB")
+        return Formatter.formatShortFileSize(this, bytes)
+    }
+
+    fun enlargeFontOfNumber(summary: CharSequence): CharSequence {
+        if (TextUtils.isEmpty(summary)) {
+            return ""
+        }
+
+        val matcher = numberPattern.matcher(summary);
+        if (matcher.find()) {
+            val spannableSummary = SpannableString(summary)
+            spannableSummary.setSpan(
+                    RelativeSizeSpan(2f),
+                    matcher.start(),
+                    matcher.end(),
+                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+            return spannableSummary
+        }
+        return summary
+    }
 }
\ No newline at end of file
diff --git a/android/TerminalApp/res/layout/settings_disk_resize.xml b/android/TerminalApp/res/layout/settings_disk_resize.xml
index 3c09f52..f868b28 100644
--- a/android/TerminalApp/res/layout/settings_disk_resize.xml
+++ b/android/TerminalApp/res/layout/settings_disk_resize.xml
@@ -21,27 +21,19 @@
         android:layout_height="match_parent">
 
         <TextView
-            android:id="@+id/settings_disk_resize_disk_size"
+            android:id="@+id/settings_disk_resize_resize_gb_assigned"
             android:layout_height="wrap_content"
             android:layout_width="wrap_content"
-            android:textSize="36sp"
-            app:layout_constraintLeft_toLeftOf="parent"
+            android:textSize="14sp"
+            app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintBottom_toTopOf="@+id/settings_disk_resize_disk_size_slider"/>
 
         <TextView
+            android:id="@+id/settings_disk_resize_resize_gb_max"
             android:layout_height="wrap_content"
             android:layout_width="wrap_content"
-            android:text="@string/settings_disk_resize_resize_gb_assigned"
             android:textSize="14sp"
-            app:layout_constraintLeft_toRightOf="@+id/settings_disk_resize_disk_size"
-            app:layout_constraintBottom_toTopOf="@+id/settings_disk_resize_disk_size_slider"/>
-
-        <TextView
-            android:layout_height="wrap_content"
-            android:layout_width="wrap_content"
-            android:text="@string/settings_disk_resize_resize_gb_total"
-            android:textSize="14sp"
-            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintBottom_toTopOf="@+id/settings_disk_resize_disk_size_slider"/>
 
         <com.google.android.material.slider.Slider
@@ -51,7 +43,6 @@
             android:layout_marginBottom="36dp"
             app:tickVisible="false"
             android:valueFrom="0"
-            android:valueTo="256"
             android:stepSize="4"
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="parent" />
@@ -65,7 +56,7 @@
             android:layout_marginHorizontal="8dp"
             app:layout_constraintTop_toTopOf="@+id/settings_disk_resize_disk_size_slider"
             app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintRight_toLeftOf="@+id/settings_disk_resize_resize_button" />
+            app:layout_constraintEnd_toStartOf="@+id/settings_disk_resize_resize_button" />
 
         <com.google.android.material.button.MaterialButton
             android:id="@+id/settings_disk_resize_resize_button"
@@ -76,6 +67,6 @@
             android:layout_marginHorizontal="8dp"
             app:layout_constraintTop_toTopOf="@+id/settings_disk_resize_disk_size_slider"
             app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintRight_toRightOf="parent" />
+            app:layout_constraintEnd_toEndOf="parent" />
     </androidx.constraintlayout.widget.ConstraintLayout>
 </LinearLayout>
\ No newline at end of file
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
index 7af9761..1cbaee8 100644
--- a/android/TerminalApp/res/values/strings.xml
+++ b/android/TerminalApp/res/values/strings.xml
@@ -36,10 +36,10 @@
     <string name="settings_disk_resize_sub_title">Resize / Rootfs</string>
     <!-- Toast message after new disk size is set. [CHAR LIMIT=none] -->
     <string name="settings_disk_resize_resize_message">Disk size set</string>
-    <!-- Settings menu option description for the current disk size, followed by a text box with the actual number [CHAR LIMIT=none] -->
-    <string name="settings_disk_resize_resize_gb_assigned">GB Assigned</string>
-    <!-- Settings menu option description for the maximum resizable disk size. [CHAR LIMIT=none] -->
-    <string name="settings_disk_resize_resize_gb_total">256 GB total</string>
+    <!-- Settings menu option description format of the current disk size. [CHAR LIMIT=none] -->
+    <string name="settings_disk_resize_resize_gb_assigned_format"><xliff:g id="assigned_size" example="10GB">%1$s</xliff:g> assigned</string>
+    <!-- Settings menu option description format of the maximum resizable disk size. [CHAR LIMIT=none] -->
+    <string name="settings_disk_resize_resize_gb_max_format"><xliff:g id="max_size" example="256GB">%1$s</xliff:g> max</string>
     <!-- Settings menu button to cancel disk resize. [CHAR LIMIT=32] -->
     <string name="settings_disk_resize_resize_cancel">Cancel</string>
     <!-- Settings menu button to apply change that requires to restart VM (abbrev of virtual machine). [CHAR LIMIT=64] -->
diff --git a/libs/libvmbase/src/entry.rs b/libs/libvmbase/src/entry.rs
index 99f28fc..f442a32 100644
--- a/libs/libvmbase/src/entry.rs
+++ b/libs/libvmbase/src/entry.rs
@@ -56,8 +56,7 @@
 /// This is the entry point to the Rust code, called from the binary entry point in `entry.S`.
 #[no_mangle]
 extern "C" fn rust_entry(x0: u64, x1: u64, x2: u64, x3: u64) -> ! {
-    // SAFETY: Only called once, from here, and inaccessible to client code.
-    unsafe { heap::init() };
+    heap::init();
 
     if try_console_init().is_err() {
         // Don't panic (or log) here to avoid accessing the console.
diff --git a/libs/libvmbase/src/heap.rs b/libs/libvmbase/src/heap.rs
index 99c06aa..3a4e198 100644
--- a/libs/libvmbase/src/heap.rs
+++ b/libs/libvmbase/src/heap.rs
@@ -22,39 +22,78 @@
 use core::ffi::c_void;
 use core::mem;
 use core::num::NonZeroUsize;
+use core::ops::Range;
 use core::ptr;
 use core::ptr::NonNull;
 
 use buddy_system_allocator::LockedHeap;
+use spin::{
+    mutex::{SpinMutex, SpinMutexGuard},
+    Once,
+};
 
 /// Configures the size of the global allocator.
 #[macro_export]
 macro_rules! configure_heap {
     ($len:expr) => {
-        static mut __HEAP_ARRAY: [u8; $len] = [0; $len];
-        #[export_name = "HEAP"]
-        // SAFETY: HEAP will only be accessed once as mut, from init().
-        static mut __HEAP: &'static mut [u8] = unsafe { &mut __HEAP_ARRAY };
+        static __HEAP: $crate::heap::HeapArray<{ $len }> = $crate::heap::HeapArray::new();
+        #[export_name = "get_heap"]
+        fn __get_heap() -> &'static mut [u8] {
+            __HEAP.get()
+        }
     };
 }
 
+/// An array to be used as a heap.
+///
+/// This should be stored in a static variable to have the appropriate lifetime.
+pub struct HeapArray<const SIZE: usize> {
+    array: SpinMutex<[u8; SIZE]>,
+}
+
+impl<const SIZE: usize> HeapArray<SIZE> {
+    /// Creates a new empty heap array.
+    #[allow(clippy::new_without_default)]
+    pub const fn new() -> Self {
+        Self { array: SpinMutex::new([0; SIZE]) }
+    }
+
+    /// Gets the heap as a slice.
+    ///
+    /// Panics if called more than once.
+    pub fn get(&self) -> &mut [u8] {
+        SpinMutexGuard::leak(self.array.try_lock().expect("Page heap was already taken"))
+            .as_mut_slice()
+    }
+}
+
 extern "Rust" {
-    /// Slice used by the global allocator, configured using configure_heap!().
-    static mut HEAP: &'static mut [u8];
+    /// Gets slice used by the global allocator, configured using configure_heap!().
+    ///
+    /// Panics if called more than once.
+    fn get_heap() -> &'static mut [u8];
 }
 
 #[global_allocator]
 static HEAP_ALLOCATOR: LockedHeap<32> = LockedHeap::<32>::new();
 
+/// The range of addresses used for the heap.
+static HEAP_RANGE: Once<Range<usize>> = Once::new();
+
 /// Initialize the global allocator.
 ///
-/// # Safety
-///
-/// Must be called no more than once.
-pub(crate) unsafe fn init() {
-    // SAFETY: Nothing else accesses this memory, and we hand it over to the heap to manage and
-    // never touch it again. The heap is locked, so there cannot be any races.
-    let (start, size) = unsafe { (HEAP.as_mut_ptr() as usize, HEAP.len()) };
+/// Panics if called more than once.
+pub(crate) fn init() {
+    // SAFETY: This is in fact a safe Rust function.
+    let heap = unsafe { get_heap() };
+
+    HEAP_RANGE.call_once(|| {
+        let range = heap.as_ptr_range();
+        range.start as usize..range.end as usize
+    });
+
+    let start = heap.as_mut_ptr() as usize;
+    let size = heap.len();
 
     let mut heap = HEAP_ALLOCATOR.lock();
     // SAFETY: We are supplying a valid memory range, and we only do this once.
@@ -107,10 +146,9 @@
 /// errors.
 unsafe extern "C" fn free(ptr: *mut c_void) {
     let Some(ptr) = NonNull::new(ptr) else { return };
-    // SAFETY: The contents of the HEAP slice may change, but the address range never does.
-    let heap_range = unsafe { HEAP.as_ptr_range() };
+    let heap_range = HEAP_RANGE.get().expect("free called before heap was initialised");
     assert!(
-        heap_range.contains(&(ptr.as_ptr() as *const u8)),
+        heap_range.contains(&(ptr.as_ptr() as usize)),
         "free() called on a pointer that is not part of the HEAP: {ptr:?}"
     );
     // SAFETY: ptr is non-null and was allocated by allocate, which prepends a correctly aligned
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 2d55d66..ffcf338 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -36,6 +36,7 @@
 import android.cts.statsdatom.lib.ReportUtils;
 
 import com.android.compatibility.common.util.CddTest;
+import com.android.compatibility.common.util.PropertyUtil;
 import com.android.compatibility.common.util.VsrTest;
 import com.android.microdroid.test.common.ProcessUtil;
 import com.android.microdroid.test.host.CommandRunner;
@@ -437,9 +438,8 @@
     @VsrTest(requirements = {"VSR-7.1-001.008"})
     public void UpgradedPackageIsAcceptedWithSecretkeeper() throws Exception {
         // Preconditions
-        assumeVmTypeSupported(true);
-        assumeUpdatableVmSupported();
-
+        assumeVmTypeSupported(true); // Non-protected VMs may not support upgrades
+        ensureUpdatableVmSupported();
         getDevice().uninstallPackage(PACKAGE_NAME);
         getDevice().installPackage(findTestFile(APK_NAME), /* reinstall= */ true);
         ensureProtectedMicrodroidBootsSuccessfully(INSTANCE_ID_FILE, INSTANCE_IMG);
@@ -1392,10 +1392,16 @@
                         && device.doesFileExist("/sys/bus/platform/drivers/vfio-platform"));
     }
 
-    private void assumeUpdatableVmSupported() throws DeviceNotAvailableException {
-        assumeTrue(
-                "This test is only applicable if if Updatable VMs are supported",
-                isUpdatableVmSupported());
+    private void ensureUpdatableVmSupported() throws DeviceNotAvailableException {
+        if (PropertyUtil.isVendorApiLevelAtLeast(getAndroidDevice(), 202504)) {
+            assertTrue(
+                    "Missing Updatable VM support, have you declared Secretkeeper interface?",
+                    isUpdatableVmSupported());
+        } else {
+            assumeTrue(
+                    "Vendor API lower than 202504 may not support Updatable VM",
+                    isUpdatableVmSupported());
+        }
     }
 
     private TestDevice getAndroidDevice() {