1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.virtualization.terminal 17 18 import android.content.Context 19 import android.content.pm.PackageManager 20 import android.os.Environment 21 import android.system.virtualmachine.VirtualMachineConfig 22 import android.system.virtualmachine.VirtualMachineCustomImageConfig 23 import android.util.DisplayMetrics 24 import android.view.WindowManager 25 import com.android.virtualization.terminal.ConfigJson.AudioJson 26 import com.android.virtualization.terminal.ConfigJson.DiskJson 27 import com.android.virtualization.terminal.ConfigJson.DisplayJson 28 import com.android.virtualization.terminal.ConfigJson.GpuJson 29 import com.android.virtualization.terminal.ConfigJson.InputJson 30 import com.android.virtualization.terminal.ConfigJson.PartitionJson 31 import com.android.virtualization.terminal.ConfigJson.SharedPathJson 32 import com.google.gson.Gson 33 import com.google.gson.annotations.SerializedName 34 import java.io.BufferedReader 35 import java.io.FileReader 36 import java.io.IOException 37 import java.io.Reader 38 import java.lang.Exception 39 import java.lang.RuntimeException 40 import java.nio.file.Files 41 import java.nio.file.Path 42 43 /** This class and its inner classes model vm_config.json. */ 44 internal data class ConfigJson( 45 @SerializedName("protected") private val isProtected: Boolean, 46 private val name: String?, 47 private val cpu_topology: String?, 48 private val platform_version: String?, 49 private val memory_mib: Int = 1024, 50 private val console_input_device: String?, 51 private val bootloader: String?, 52 private val kernel: String?, 53 private val initrd: String?, 54 private val params: String?, 55 private val debuggable: Boolean, 56 private val console_out: Boolean, 57 private val connect_console: Boolean, 58 private val network: Boolean, 59 private val input: InputJson?, 60 private val audio: AudioJson?, 61 private val disks: Array<DiskJson>?, 62 private val sharedPath: Array<SharedPathJson>?, 63 private val display: DisplayJson?, 64 private val gpu: GpuJson?, 65 private val auto_memory_balloon: Boolean, 66 ) { 67 private fun getCpuTopology(): Int { 68 return when (cpu_topology) { 69 "one_cpu" -> VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU 70 "match_host" -> VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST 71 else -> throw RuntimeException("invalid cpu topology: $cpu_topology") 72 } 73 } 74 75 private fun getDebugLevel(): Int { 76 return if (debuggable) VirtualMachineConfig.DEBUG_LEVEL_FULL 77 else VirtualMachineConfig.DEBUG_LEVEL_NONE 78 } 79 80 /** Converts this parsed JSON into VirtualMachineConfig Builder */ 81 fun toConfigBuilder(context: Context): VirtualMachineConfig.Builder { 82 return VirtualMachineConfig.Builder(context) 83 .setProtectedVm(isProtected) 84 .setMemoryBytes(memory_mib.toLong() * 1024 * 1024) 85 .setConsoleInputDevice(console_input_device) 86 .setCpuTopology(getCpuTopology()) 87 .setCustomImageConfig(toCustomImageConfigBuilder(context).build()) 88 .setDebugLevel(getDebugLevel()) 89 .setVmOutputCaptured(console_out) 90 .setConnectVmConsole(connect_console) 91 } 92 93 fun toCustomImageConfigBuilder(context: Context): VirtualMachineCustomImageConfig.Builder { 94 val builder = VirtualMachineCustomImageConfig.Builder() 95 96 builder 97 .setName(name) 98 .setBootloaderPath(bootloader) 99 .setKernelPath(kernel) 100 .setInitrdPath(initrd) 101 .useNetwork(network) 102 .useAutoMemoryBalloon(auto_memory_balloon) 103 104 if (input != null) { 105 builder 106 .useTouch(input.touchscreen) 107 .useKeyboard(input.keyboard) 108 .useMouse(input.mouse) 109 .useTrackpad(input.trackpad) 110 .useSwitches(input.switches) 111 } 112 113 if (audio != null) { 114 builder.setAudioConfig(audio.toConfig()) 115 } 116 117 if (display != null) { 118 builder.setDisplayConfig(display.toConfig(context)) 119 } 120 121 if (gpu != null) { 122 builder.setGpuConfig(gpu.toConfig()) 123 } 124 125 params?.split(" ".toRegex())?.filter { it.isNotEmpty() }?.forEach { builder.addParam(it) } 126 127 disks?.forEach { builder.addDisk(it.toConfig()) } 128 129 sharedPath?.mapNotNull { it.toConfig(context) }?.forEach { builder.addSharedPath(it) } 130 131 return builder 132 } 133 134 internal data class SharedPathJson(private val sharedPath: String?) { 135 fun toConfig(context: Context): VirtualMachineCustomImageConfig.SharedPath? { 136 try { 137 val terminalUid = getTerminalUid(context) 138 if (sharedPath?.contains("emulated") == true) { 139 if (Environment.isExternalStorageManager()) { 140 val currentUserId = context.userId 141 val path = "$sharedPath/$currentUserId/Download" 142 return VirtualMachineCustomImageConfig.SharedPath( 143 path, 144 terminalUid, 145 terminalUid, 146 GUEST_UID, 147 GUEST_GID, 148 7, 149 "android", 150 "android", 151 false, /* app domain is set to false so that crosvm is spin up as child of virtmgr */ 152 "", 153 ) 154 } 155 return null 156 } 157 val socketPath = context.getFilesDir().toPath().resolve("internal.virtiofs") 158 Files.deleteIfExists(socketPath) 159 return VirtualMachineCustomImageConfig.SharedPath( 160 sharedPath, 161 terminalUid, 162 terminalUid, 163 0, 164 0, 165 7, 166 "internal", 167 "internal", 168 true, /* app domain is set to true so that crosvm is spin up from app context */ 169 socketPath.toString(), 170 ) 171 } catch (e: PackageManager.NameNotFoundException) { 172 return null 173 } catch (e: IOException) { 174 return null 175 } 176 } 177 178 @Throws(PackageManager.NameNotFoundException::class) 179 fun getTerminalUid(context: Context): Int { 180 return context 181 .getPackageManager() 182 .getPackageUidAsUser(context.getPackageName(), context.userId) 183 } 184 185 companion object { 186 private const val GUEST_UID = 1000 187 private const val GUEST_GID = 100 188 } 189 } 190 191 internal data class InputJson( 192 val touchscreen: Boolean, 193 val keyboard: Boolean, 194 val mouse: Boolean, 195 val switches: Boolean, 196 val trackpad: Boolean, 197 ) 198 199 internal data class AudioJson(private val microphone: Boolean, private val speaker: Boolean) { 200 201 fun toConfig(): VirtualMachineCustomImageConfig.AudioConfig { 202 return VirtualMachineCustomImageConfig.AudioConfig.Builder() 203 .setUseMicrophone(microphone) 204 .setUseSpeaker(speaker) 205 .build() 206 } 207 } 208 209 internal data class DiskJson( 210 private val writable: Boolean, 211 private val image: String?, 212 private val partitions: Array<PartitionJson>?, 213 ) { 214 fun toConfig(): VirtualMachineCustomImageConfig.Disk { 215 val d = 216 if (writable) VirtualMachineCustomImageConfig.Disk.RWDisk(image) 217 else VirtualMachineCustomImageConfig.Disk.RODisk(image) 218 partitions?.forEach { 219 val writable = this.writable && it.writable 220 d.addPartition( 221 VirtualMachineCustomImageConfig.Partition(it.label, it.path, writable, it.guid) 222 ) 223 } 224 return d 225 } 226 } 227 228 internal data class PartitionJson( 229 val writable: Boolean, 230 val label: String?, 231 val path: String?, 232 val guid: String?, 233 ) 234 235 internal data class DisplayJson( 236 private val scale: Float = 0f, 237 private val refresh_rate: Int = 0, 238 private val width_pixels: Int = 0, 239 private val height_pixels: Int = 0, 240 ) { 241 fun toConfig(context: Context): VirtualMachineCustomImageConfig.DisplayConfig { 242 val wm = context.getSystemService<WindowManager>(WindowManager::class.java) 243 val metrics = wm.currentWindowMetrics 244 val dispBounds = metrics.bounds 245 246 val width = if (width_pixels > 0) width_pixels else dispBounds.right 247 val height = if (height_pixels > 0) height_pixels else dispBounds.bottom 248 249 var dpi = (DisplayMetrics.DENSITY_DEFAULT * metrics.density).toInt() 250 if (scale > 0.0f) { 251 dpi = (dpi * scale).toInt() 252 } 253 254 var refreshRate = context.display.refreshRate.toInt() 255 if (this.refresh_rate != 0) { 256 refreshRate = this.refresh_rate 257 } 258 259 return VirtualMachineCustomImageConfig.DisplayConfig.Builder() 260 .setWidth(width) 261 .setHeight(height) 262 .setHorizontalDpi(dpi) 263 .setVerticalDpi(dpi) 264 .setRefreshRate(refreshRate) 265 .build() 266 } 267 } 268 269 internal data class GpuJson( 270 private val backend: String?, 271 private val pci_address: String?, 272 private val renderer_features: String?, 273 // TODO: GSON actaully ignores the default values 274 private val renderer_use_egl: Boolean = true, 275 private val renderer_use_gles: Boolean = true, 276 private val renderer_use_glx: Boolean = false, 277 private val renderer_use_surfaceless: Boolean = true, 278 private val renderer_use_vulkan: Boolean = false, 279 private val context_types: Array<String>?, 280 ) { 281 fun toConfig(): VirtualMachineCustomImageConfig.GpuConfig { 282 return VirtualMachineCustomImageConfig.GpuConfig.Builder() 283 .setBackend(backend) 284 .setPciAddress(pci_address) 285 .setRendererFeatures(renderer_features) 286 .setRendererUseEgl(renderer_use_egl) 287 .setRendererUseGles(renderer_use_gles) 288 .setRendererUseGlx(renderer_use_glx) 289 .setRendererUseSurfaceless(renderer_use_surfaceless) 290 .setRendererUseVulkan(renderer_use_vulkan) 291 .setContextTypes(context_types) 292 .build() 293 } 294 } 295 296 companion object { 297 private const val DEBUG = true 298 299 /** Parses JSON file at jsonPath */ 300 fun from(context: Context, jsonPath: Path): ConfigJson { 301 try { 302 FileReader(jsonPath.toFile()).use { fileReader -> 303 val content = replaceKeywords(fileReader, context) 304 return Gson().fromJson<ConfigJson?>(content, ConfigJson::class.java) 305 } 306 } catch (e: Exception) { 307 throw RuntimeException("Failed to parse $jsonPath", e) 308 } 309 } 310 311 @Throws(IOException::class) 312 private fun replaceKeywords(r: Reader, context: Context): String { 313 val rules: Map<String, String> = 314 mapOf( 315 "\\\$PAYLOAD_DIR" to InstalledImage.getDefault(context).installDir.toString(), 316 "\\\$USER_ID" to context.userId.toString(), 317 "\\\$PACKAGE_NAME" to context.getPackageName(), 318 "\\\$APP_DATA_DIR" to context.getDataDir().toString(), 319 ) 320 321 return BufferedReader(r).useLines { lines -> 322 lines 323 .map { line -> 324 rules.entries.fold(line) { acc, rule -> 325 acc.replace(rule.key.toRegex(), rule.value) 326 } 327 } 328 .joinToString("\n") 329 } 330 } 331 } 332 } 333