Add display config for CustomImageConfig

For now, display config include display size info and refresh rate

Bug: 331708504
Test: check if the screen shows
Change-Id: I0f41822c756b56d17efa6150f0c3ec28628371d4
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
index be80db8..a8f318c 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -641,6 +641,10 @@
             config.disks[i].partitions = new Partition[0];
         }
 
+        config.displayConfig =
+                Optional.ofNullable(customImageConfig.getDisplayConfig())
+                        .map(dc -> dc.toParcelable())
+                        .orElse(null);
         config.protectedVm = this.mProtectedVm;
         config.memoryMib = bytesToMebiBytes(mMemoryBytes);
         config.cpuTopology = (byte) this.mCpuTopology;
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
index 7a4f564..89df1f2 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -22,6 +22,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 
 /** @hide */
 public class VirtualMachineCustomImageConfig {
@@ -32,12 +33,15 @@
     private static final String KEY_PARAMS = "params";
     private static final String KEY_DISK_WRITABLES = "disk_writables";
     private static final String KEY_DISK_IMAGES = "disk_images";
+    private static final String KEY_DISPLAY_CONFIG = "display_config";
+
     @Nullable private final String name;
     @NonNull private final String kernelPath;
     @Nullable private final String initrdPath;
     @Nullable private final String bootloaderPath;
     @Nullable private final String[] params;
     @Nullable private final Disk[] disks;
+    @Nullable private final DisplayConfig displayConfig;
 
     @Nullable
     public Disk[] getDisks() {
@@ -76,13 +80,15 @@
             String initrdPath,
             String bootloaderPath,
             String[] params,
-            Disk[] disks) {
+            Disk[] disks,
+            DisplayConfig displayConfig) {
         this.name = name;
         this.kernelPath = kernelPath;
         this.initrdPath = initrdPath;
         this.bootloaderPath = bootloaderPath;
         this.params = params;
         this.disks = disks;
+        this.displayConfig = displayConfig;
     }
 
     static VirtualMachineCustomImageConfig from(PersistableBundle customImageConfigBundle) {
@@ -107,9 +113,15 @@
                 }
             }
         }
+        PersistableBundle displayConfigPb =
+                customImageConfigBundle.getPersistableBundle(KEY_DISPLAY_CONFIG);
+        builder.setDisplayConfig(DisplayConfig.from(displayConfigPb));
+
         return builder.build();
     }
 
+
+
     PersistableBundle toPersistableBundle() {
         PersistableBundle pb = new PersistableBundle();
         pb.putString(KEY_NAME, this.name);
@@ -128,9 +140,19 @@
             pb.putBooleanArray(KEY_DISK_WRITABLES, writables);
             pb.putStringArray(KEY_DISK_IMAGES, images);
         }
+        pb.putPersistableBundle(
+                KEY_DISPLAY_CONFIG,
+                Optional.ofNullable(displayConfig)
+                        .map(dc -> dc.toPersistableBundle())
+                        .orElse(null));
         return pb;
     }
 
+    @Nullable
+    public DisplayConfig getDisplayConfig() {
+        return displayConfig;
+    }
+
     /** @hide */
     public static final class Disk {
         private final boolean writable;
@@ -170,6 +192,7 @@
         private String bootloaderPath;
         private List<String> params = new ArrayList<>();
         private List<Disk> disks = new ArrayList<>();
+        private DisplayConfig displayConfig;
 
         /** @hide */
         public Builder() {}
@@ -211,6 +234,12 @@
         }
 
         /** @hide */
+        public Builder setDisplayConfig(DisplayConfig displayConfig) {
+            this.displayConfig = displayConfig;
+            return this;
+        }
+
+        /** @hide */
         public VirtualMachineCustomImageConfig build() {
             return new VirtualMachineCustomImageConfig(
                     this.name,
@@ -218,7 +247,142 @@
                     this.initrdPath,
                     this.bootloaderPath,
                     this.params.toArray(new String[0]),
-                    this.disks.toArray(new Disk[0]));
+                    this.disks.toArray(new Disk[0]),
+                    displayConfig);
+        }
+    }
+
+    /** @hide */
+    public static final class DisplayConfig {
+        private static final String KEY_WIDTH = "width";
+        private static final String KEY_HEIGHT = "height";
+        private static final String KEY_HORIZONTAL_DPI = "horizontal_dpi";
+        private static final String KEY_VERTICAL_DPI = "vertical_dpi";
+        private static final String KEY_REFRESH_RATE = "refresh_rate";
+        private final int width;
+        private final int height;
+        private final int horizontalDpi;
+        private final int verticalDpi;
+        private final int refreshRate;
+
+        private DisplayConfig(
+                int width, int height, int horizontalDpi, int verticalDpi, int refreshRate) {
+            this.width = width;
+            this.height = height;
+            this.horizontalDpi = horizontalDpi;
+            this.verticalDpi = verticalDpi;
+            this.refreshRate = refreshRate;
+        }
+
+        /** @hide */
+        public int getWidth() {
+            return width;
+        }
+
+        /** @hide */
+        public int getHeight() {
+            return height;
+        }
+
+        /** @hide */
+        public int getHorizontalDpi() {
+            return horizontalDpi;
+        }
+
+        /** @hide */
+        public int getVerticalDpi() {
+            return verticalDpi;
+        }
+
+        /** @hide */
+        public int getRefreshRate() {
+            return refreshRate;
+        }
+
+        android.system.virtualizationservice.DisplayConfig toParcelable() {
+            android.system.virtualizationservice.DisplayConfig parcelable =
+                    new android.system.virtualizationservice.DisplayConfig();
+            parcelable.width = this.width;
+            parcelable.height = this.height;
+            parcelable.horizontalDpi = this.horizontalDpi;
+            parcelable.verticalDpi = this.verticalDpi;
+            parcelable.refreshRate = this.refreshRate;
+
+            return parcelable;
+        }
+
+        private static DisplayConfig from(PersistableBundle pb) {
+            if (pb == null) {
+                return null;
+            }
+            Builder builder = new Builder();
+            builder.setWidth(pb.getInt(KEY_WIDTH));
+            builder.setHeight(pb.getInt(KEY_HEIGHT));
+            builder.setHorizontalDpi(pb.getInt(KEY_HORIZONTAL_DPI));
+            builder.setVerticalDpi(pb.getInt(KEY_VERTICAL_DPI));
+            builder.setRefreshRate(pb.getInt(KEY_REFRESH_RATE));
+            return builder.build();
+        }
+
+        private PersistableBundle toPersistableBundle() {
+            PersistableBundle pb = new PersistableBundle();
+            pb.putInt(KEY_WIDTH, this.width);
+            pb.putInt(KEY_HEIGHT, this.height);
+            pb.putInt(KEY_HORIZONTAL_DPI, this.horizontalDpi);
+            pb.putInt(KEY_VERTICAL_DPI, this.verticalDpi);
+            pb.putInt(KEY_REFRESH_RATE, this.refreshRate);
+            return pb;
+        }
+
+        /** @hide */
+        public static class Builder {
+            // Default values come from external/crosvm/vm_control/src/gpu.rs
+            private int width;
+            private int height;
+            private int horizontalDpi = 320;
+            private int verticalDpi = 320;
+            private int refreshRate = 60;
+
+            /** @hide */
+            public Builder() {}
+
+            /** @hide */
+            public Builder setWidth(int width) {
+                this.width = width;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setHeight(int height) {
+                this.height = height;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setHorizontalDpi(int horizontalDpi) {
+                this.horizontalDpi = horizontalDpi;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setVerticalDpi(int verticalDpi) {
+                this.verticalDpi = verticalDpi;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRefreshRate(int refreshRate) {
+                this.refreshRate = refreshRate;
+                return this;
+            }
+
+            /** @hide */
+            public DisplayConfig build() {
+                if (this.width == 0 || this.height == 0) {
+                    throw new IllegalStateException("width and height must be specified");
+                }
+                return new DisplayConfig(width, height, horizontalDpi, verticalDpi, refreshRate);
+            }
         }
     }
 }
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index b8c6315..d65df34 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -17,7 +17,7 @@
 use crate::{get_calling_pid, get_calling_uid};
 use crate::atom::{write_vm_booted_stats, write_vm_creation_stats};
 use crate::composite::make_composite_image;
-use crate::crosvm::{CrosvmConfig, DiskFile, PayloadState, VmContext, VmInstance, VmState};
+use crate::crosvm::{CrosvmConfig, DiskFile, DisplayConfig, PayloadState, VmContext, VmInstance, VmState};
 use crate::debug_config::DebugConfig;
 use crate::dt_overlay::{create_device_tree_overlay, VM_DT_OVERLAY_MAX_SIZE, VM_DT_OVERLAY_PATH};
 use crate::payload::{add_microdroid_payload_images, add_microdroid_system_images, add_microdroid_vendor_image};
@@ -571,6 +571,13 @@
             (vec![], None)
         };
 
+        let display_config = config
+            .displayConfig
+            .as_ref()
+            .map(DisplayConfig::new)
+            .transpose()
+            .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT)?;
+
         // Actually start the VM.
         let crosvm_config = CrosvmConfig {
             cid,
@@ -596,6 +603,7 @@
             vfio_devices,
             dtbo,
             device_tree_overlay,
+            display_config,
         };
         let instance = Arc::new(
             VmInstance::new(
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 97a27e0..d5e2e67 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -44,7 +44,8 @@
 use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::DeathReason::DeathReason;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
     MemoryTrimLevel::MemoryTrimLevel,
-    VirtualMachineAppConfig::DebugLevel::DebugLevel
+    VirtualMachineAppConfig::DebugLevel::DebugLevel,
+    DisplayConfig::DisplayConfig as DisplayConfigParcelable,
 };
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IGlobalVmContext::IGlobalVmContext;
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IBoundDevice::IBoundDevice;
@@ -118,6 +119,32 @@
     pub vfio_devices: Vec<VfioDevice>,
     pub dtbo: Option<File>,
     pub device_tree_overlay: Option<File>,
+    pub display_config: Option<DisplayConfig>,
+}
+
+#[derive(Debug)]
+pub struct DisplayConfig {
+    pub width: NonZeroU32,
+    pub height: NonZeroU32,
+    pub horizontal_dpi: NonZeroU32,
+    pub vertical_dpi: NonZeroU32,
+    pub refresh_rate: NonZeroU32,
+}
+
+impl DisplayConfig {
+    pub fn new(raw_config: &DisplayConfigParcelable) -> Result<DisplayConfig> {
+        let width = try_into_non_zero_u32(raw_config.width)?;
+        let height = try_into_non_zero_u32(raw_config.height)?;
+        let horizontal_dpi = try_into_non_zero_u32(raw_config.horizontalDpi)?;
+        let vertical_dpi = try_into_non_zero_u32(raw_config.verticalDpi)?;
+        let refresh_rate = try_into_non_zero_u32(raw_config.refreshRate)?;
+        Ok(DisplayConfig { width, height, horizontal_dpi, vertical_dpi, refresh_rate })
+    }
+}
+
+fn try_into_non_zero_u32(value: i32) -> Result<NonZeroU32> {
+    let u32_value = value.try_into()?;
+    NonZeroU32::new(u32_value).ok_or(anyhow!("value should be greater than 0"))
 }
 
 /// A disk image to pass to crosvm for a VM.
@@ -927,6 +954,13 @@
     if let Some(dt_overlay) = &config.device_tree_overlay {
         command.arg("--device-tree-overlay").arg(add_preserved_fd(&mut preserved_fds, dt_overlay));
     }
+    if let Some(display_config) = &config.display_config {
+        command.arg("--gpu")
+        // TODO(b/331708504): support backend config as well
+        .arg("backend=virglrenderer,context-types=virgl2,egl=true,surfaceless=true,glx=false,gles=true")
+        .arg(format!("--gpu-display=mode=windowed[{},{}],dpi=[{},{}],refresh-rate={}", display_config.width, display_config.height, display_config.horizontal_dpi, display_config.vertical_dpi, display_config.refresh_rate))
+        .arg(format!("--android-display-service={}", config.name));
+    }
 
     append_platform_devices(&mut command, &mut preserved_fds, &config)?;
 
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/DisplayConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/DisplayConfig.aidl
new file mode 100644
index 0000000..1fd392b
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/DisplayConfig.aidl
@@ -0,0 +1,24 @@
+/*
+ * 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 android.system.virtualizationservice;
+
+parcelable DisplayConfig {
+    int width;
+    int height;
+    int horizontalDpi;
+    int verticalDpi;
+    int refreshRate;
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index b2116c4..1a18bf8 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -17,6 +17,7 @@
 
 import android.system.virtualizationservice.CpuTopology;
 import android.system.virtualizationservice.DiskImage;
+import android.system.virtualizationservice.DisplayConfig;
 
 /** Raw configuration for running a VM. */
 parcelable VirtualMachineRawConfig {
@@ -70,4 +71,6 @@
 
     /** List of SysFS nodes of devices to be assigned */
     String[] devices;
+
+    @nullable DisplayConfig displayConfig;
 }
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index 2af1e2f..d2e03f5 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -19,23 +19,28 @@
 import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
 
 import android.app.Activity;
+import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.crosvm.ICrosvmAndroidDisplayService;
 import android.system.virtualizationservice_internal.IVirtualizationServiceInternal;
 import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
+import android.util.DisplayMetrics;
 import android.util.Log;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
 import android.system.virtualmachine.VirtualMachineConfig;
 import android.system.virtualmachine.VirtualMachineException;
 import android.system.virtualmachine.VirtualMachineManager;
+import android.view.Display;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import android.view.WindowManager;
 import android.view.WindowInsets;
 import android.view.WindowInsetsController;
+import android.view.WindowMetrics;
 
 import org.json.JSONArray;
 import org.json.JSONException;
@@ -106,6 +111,21 @@
             }
 
             configBuilder.setMemoryBytes(8L * 1024 * 1024 * 1024 /* 8 GB */);
+            WindowMetrics windowMetrics = getWindowManager().getCurrentWindowMetrics();
+            Rect windowSize = windowMetrics.getBounds();
+            int dpi = (int) (DisplayMetrics.DENSITY_DEFAULT * windowMetrics.getDensity());
+            DisplayConfig.Builder displayConfigBuilder = new DisplayConfig.Builder();
+            displayConfigBuilder.setWidth(windowSize.right);
+            displayConfigBuilder.setHeight(windowSize.bottom);
+            displayConfigBuilder.setHorizontalDpi(dpi);
+            displayConfigBuilder.setVerticalDpi(dpi);
+
+            Display display = getDisplay();
+            if (display != null) {
+                displayConfigBuilder.setRefreshRate((int) display.getRefreshRate());
+            }
+
+            customImageConfigBuilder.setDisplayConfig(displayConfigBuilder.build());
             configBuilder.setCustomImageConfig(customImageConfigBuilder.build());
 
         } catch (JSONException | IOException e) {