• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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