1 /* 2 * 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 17 package com.android.virtualization.vmlauncher; 18 19 import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST; 20 21 import android.app.Activity; 22 import android.crosvm.ICrosvmAndroidDisplayService; 23 import android.graphics.PixelFormat; 24 import android.graphics.Rect; 25 import android.os.Bundle; 26 import android.os.ParcelFileDescriptor; 27 import android.os.RemoteException; 28 import android.os.ServiceManager; 29 import android.system.virtualizationservice_internal.IVirtualizationServiceInternal; 30 import android.system.virtualmachine.VirtualMachine; 31 import android.system.virtualmachine.VirtualMachineCallback; 32 import android.system.virtualmachine.VirtualMachineConfig; 33 import android.system.virtualmachine.VirtualMachineCustomImageConfig; 34 import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig; 35 import android.system.virtualmachine.VirtualMachineCustomImageConfig.GpuConfig; 36 import android.system.virtualmachine.VirtualMachineException; 37 import android.system.virtualmachine.VirtualMachineManager; 38 import android.util.DisplayMetrics; 39 import android.util.Log; 40 import android.view.Display; 41 import android.view.InputDevice; 42 import android.view.KeyEvent; 43 import android.view.SurfaceHolder; 44 import android.view.SurfaceView; 45 import android.view.View; 46 import android.view.WindowInsets; 47 import android.view.WindowInsetsController; 48 import android.view.WindowManager; 49 import android.view.WindowMetrics; 50 51 import libcore.io.IoBridge; 52 53 import org.json.JSONArray; 54 import org.json.JSONException; 55 import org.json.JSONObject; 56 57 import java.io.BufferedOutputStream; 58 import java.io.BufferedReader; 59 import java.io.IOException; 60 import java.io.InputStream; 61 import java.io.InputStreamReader; 62 import java.io.OutputStream; 63 import java.nio.ByteBuffer; 64 import java.nio.ByteOrder; 65 import java.nio.file.Files; 66 import java.nio.file.Path; 67 import java.util.ArrayList; 68 import java.util.Arrays; 69 import java.util.concurrent.ExecutorService; 70 import java.util.concurrent.Executors; 71 72 public class MainActivity extends Activity { 73 private static final String TAG = "VmLauncherApp"; 74 private static final String VM_NAME = "my_custom_vm"; 75 private static final boolean DEBUG = true; 76 private ExecutorService mExecutorService; 77 private VirtualMachine mVirtualMachine; 78 private ParcelFileDescriptor mCursorStream; 79 createVirtualMachineConfig(String jsonPath)80 private VirtualMachineConfig createVirtualMachineConfig(String jsonPath) { 81 VirtualMachineConfig.Builder configBuilder = 82 new VirtualMachineConfig.Builder(getApplication()); 83 configBuilder.setCpuTopology(CPU_TOPOLOGY_MATCH_HOST); 84 85 configBuilder.setProtectedVm(false); 86 if (DEBUG) { 87 configBuilder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL); 88 configBuilder.setVmOutputCaptured(true); 89 configBuilder.setConnectVmConsole(true); 90 } 91 VirtualMachineCustomImageConfig.Builder customImageConfigBuilder = 92 new VirtualMachineCustomImageConfig.Builder(); 93 try { 94 String rawJson = new String(Files.readAllBytes(Path.of(jsonPath))); 95 JSONObject json = new JSONObject(rawJson); 96 customImageConfigBuilder.setName(json.optString("name", "")); 97 if (json.has("kernel")) { 98 customImageConfigBuilder.setKernelPath(json.getString("kernel")); 99 } 100 if (json.has("initrd")) { 101 customImageConfigBuilder.setInitrdPath(json.getString("initrd")); 102 } 103 if (json.has("params")) { 104 Arrays.stream(json.getString("params").split(" ")) 105 .forEach(customImageConfigBuilder::addParam); 106 } 107 if (json.has("bootloader")) { 108 customImageConfigBuilder.setBootloaderPath(json.getString("bootloader")); 109 } 110 if (json.has("disks")) { 111 JSONArray diskArr = json.getJSONArray("disks"); 112 for (int i = 0; i < diskArr.length(); i++) { 113 JSONObject item = diskArr.getJSONObject(i); 114 if (item.has("image")) { 115 if (item.optBoolean("writable", false)) { 116 customImageConfigBuilder.addDisk( 117 VirtualMachineCustomImageConfig.Disk.RWDisk( 118 item.getString("image"))); 119 } else { 120 customImageConfigBuilder.addDisk( 121 VirtualMachineCustomImageConfig.Disk.RODisk( 122 item.getString("image"))); 123 } 124 } 125 } 126 } 127 if (json.has("console_input_device")) { 128 configBuilder.setConsoleInputDevice(json.getString("console_input_device")); 129 } 130 if (json.has("gpu")) { 131 JSONObject gpuJson = json.getJSONObject("gpu"); 132 133 GpuConfig.Builder gpuConfigBuilder = new GpuConfig.Builder(); 134 135 if (gpuJson.has("backend")) { 136 gpuConfigBuilder.setBackend(gpuJson.getString("backend")); 137 } 138 if (gpuJson.has("context_types")) { 139 ArrayList<String> contextTypes = new ArrayList<String>(); 140 JSONArray contextTypesJson = gpuJson.getJSONArray("context_types"); 141 for (int i = 0; i < contextTypesJson.length(); i++) { 142 contextTypes.add(contextTypesJson.getString(i)); 143 } 144 gpuConfigBuilder.setContextTypes(contextTypes.toArray(new String[0])); 145 } 146 if (gpuJson.has("pci_address")) { 147 gpuConfigBuilder.setPciAddress(gpuJson.getString("pci_address")); 148 } 149 if (gpuJson.has("renderer_features")) { 150 gpuConfigBuilder.setRendererFeatures(gpuJson.getString("renderer_features")); 151 } 152 if (gpuJson.has("renderer_use_egl")) { 153 gpuConfigBuilder.setRendererUseEgl(gpuJson.getBoolean("renderer_use_egl")); 154 } 155 if (gpuJson.has("renderer_use_gles")) { 156 gpuConfigBuilder.setRendererUseGles(gpuJson.getBoolean("renderer_use_gles")); 157 } 158 if (gpuJson.has("renderer_use_glx")) { 159 gpuConfigBuilder.setRendererUseGlx(gpuJson.getBoolean("renderer_use_glx")); 160 } 161 if (gpuJson.has("renderer_use_surfaceless")) { 162 gpuConfigBuilder.setRendererUseSurfaceless( 163 gpuJson.getBoolean("renderer_use_surfaceless")); 164 } 165 if (gpuJson.has("renderer_use_vulkan")) { 166 gpuConfigBuilder.setRendererUseVulkan( 167 gpuJson.getBoolean("renderer_use_vulkan")); 168 } 169 customImageConfigBuilder.setGpuConfig(gpuConfigBuilder.build()); 170 } 171 172 configBuilder.setMemoryBytes(8L * 1024 * 1024 * 1024 /* 8 GB */); 173 WindowMetrics windowMetrics = getWindowManager().getCurrentWindowMetrics(); 174 Rect windowSize = windowMetrics.getBounds(); 175 int dpi = (int) (DisplayMetrics.DENSITY_DEFAULT * windowMetrics.getDensity()); 176 DisplayConfig.Builder displayConfigBuilder = new DisplayConfig.Builder(); 177 displayConfigBuilder.setWidth(windowSize.right); 178 displayConfigBuilder.setHeight(windowSize.bottom); 179 displayConfigBuilder.setHorizontalDpi(dpi); 180 displayConfigBuilder.setVerticalDpi(dpi); 181 182 Display display = getDisplay(); 183 if (display != null) { 184 displayConfigBuilder.setRefreshRate((int) display.getRefreshRate()); 185 } 186 187 customImageConfigBuilder.setDisplayConfig(displayConfigBuilder.build()); 188 customImageConfigBuilder.useTouch(true); 189 customImageConfigBuilder.useKeyboard(true); 190 customImageConfigBuilder.useMouse(true); 191 192 configBuilder.setCustomImageConfig(customImageConfigBuilder.build()); 193 194 } catch (JSONException | IOException e) { 195 throw new IllegalStateException("malformed input", e); 196 } 197 return configBuilder.build(); 198 } 199 200 @Override onKeyDown(int keyCode, KeyEvent event)201 public boolean onKeyDown(int keyCode, KeyEvent event) { 202 if (mVirtualMachine == null) { 203 return false; 204 } 205 return mVirtualMachine.sendKeyEvent(event); 206 } 207 208 @Override onKeyUp(int keyCode, KeyEvent event)209 public boolean onKeyUp(int keyCode, KeyEvent event) { 210 if (mVirtualMachine == null) { 211 return false; 212 } 213 return mVirtualMachine.sendKeyEvent(event); 214 } 215 216 @Override onCreate(Bundle savedInstanceState)217 protected void onCreate(Bundle savedInstanceState) { 218 super.onCreate(savedInstanceState); 219 mExecutorService = Executors.newCachedThreadPool(); 220 try { 221 // To ensure that the previous display service is removed. 222 IVirtualizationServiceInternal.Stub.asInterface( 223 ServiceManager.waitForService("android.system.virtualizationservice")) 224 .clearDisplayService(); 225 } catch (RemoteException e) { 226 Log.d(TAG, "failed to clearDisplayService"); 227 } 228 getWindow().setDecorFitsSystemWindows(false); 229 setContentView(R.layout.activity_main); 230 VirtualMachineCallback callback = 231 new VirtualMachineCallback() { 232 // store reference to ExecutorService to avoid race condition 233 private final ExecutorService mService = mExecutorService; 234 235 @Override 236 public void onPayloadStarted(VirtualMachine vm) { 237 Log.e(TAG, "payload start"); 238 } 239 240 @Override 241 public void onPayloadReady(VirtualMachine vm) { 242 // This check doesn't 100% prevent race condition or UI hang. 243 // However, it's fine for demo. 244 if (mService.isShutdown()) { 245 return; 246 } 247 Log.d(TAG, "(Payload is ready. Testing VM service...)"); 248 } 249 250 @Override 251 public void onPayloadFinished(VirtualMachine vm, int exitCode) { 252 // This check doesn't 100% prevent race condition, but is fine for demo. 253 if (!mService.isShutdown()) { 254 Log.d( 255 TAG, 256 String.format("(Payload finished. exit code: %d)", exitCode)); 257 } 258 } 259 260 @Override 261 public void onError(VirtualMachine vm, int errorCode, String message) { 262 Log.d( 263 TAG, 264 String.format( 265 "(Error occurred. code: %d, message: %s)", 266 errorCode, message)); 267 } 268 269 @Override 270 public void onStopped(VirtualMachine vm, int reason) { 271 Log.e(TAG, "vm stop"); 272 } 273 }; 274 275 try { 276 VirtualMachineConfig config = 277 createVirtualMachineConfig("/data/local/tmp/vm_config.json"); 278 VirtualMachineManager vmm = 279 getApplication().getSystemService(VirtualMachineManager.class); 280 if (vmm == null) { 281 Log.e(TAG, "vmm is null"); 282 return; 283 } 284 mVirtualMachine = vmm.getOrCreate(VM_NAME, config); 285 try { 286 mVirtualMachine.setConfig(config); 287 } catch (VirtualMachineException e) { 288 vmm.delete(VM_NAME); 289 mVirtualMachine = vmm.create(VM_NAME, config); 290 Log.e(TAG, "error" + e); 291 } 292 293 Log.d(TAG, "vm start"); 294 mVirtualMachine.run(); 295 mVirtualMachine.setCallback(Executors.newSingleThreadExecutor(), callback); 296 if (DEBUG) { 297 InputStream console = mVirtualMachine.getConsoleOutput(); 298 InputStream log = mVirtualMachine.getLogOutput(); 299 OutputStream consoleLogFile = 300 new LineBufferedOutputStream( 301 getApplicationContext().openFileOutput("console.log", 0)); 302 mExecutorService.execute(new CopyStreamTask("console", console, consoleLogFile)); 303 mExecutorService.execute(new Reader("log", log)); 304 } 305 } catch (VirtualMachineException | IOException e) { 306 throw new RuntimeException(e); 307 } 308 309 SurfaceView surfaceView = findViewById(R.id.surface_view); 310 SurfaceView cursorSurfaceView = findViewById(R.id.cursor_surface_view); 311 cursorSurfaceView.setZOrderMediaOverlay(true); 312 View backgroundTouchView = findViewById(R.id.background_touch_view); 313 backgroundTouchView.setOnTouchListener( 314 (v, event) -> { 315 if (mVirtualMachine == null) { 316 return false; 317 } 318 return mVirtualMachine.sendSingleTouchEvent(event); 319 }); 320 surfaceView.requestUnbufferedDispatch(InputDevice.SOURCE_ANY); 321 surfaceView.setOnCapturedPointerListener( 322 (v, event) -> { 323 if (mVirtualMachine == null) { 324 return false; 325 } 326 return mVirtualMachine.sendMouseEvent(event); 327 }); 328 surfaceView 329 .getHolder() 330 .addCallback( 331 // TODO(b/331708504): it should be handled in AVF framework. 332 new SurfaceHolder.Callback() { 333 @Override 334 public void surfaceCreated(SurfaceHolder holder) { 335 Log.d( 336 TAG, 337 "surface size: " 338 + holder.getSurfaceFrame().flattenToString()); 339 Log.d( 340 TAG, 341 "ICrosvmAndroidDisplayService.setSurface(" 342 + holder.getSurface() 343 + ")"); 344 runWithDisplayService( 345 (service) -> 346 service.setSurface( 347 holder.getSurface(), 348 false /* forCursor */)); 349 } 350 351 @Override 352 public void surfaceChanged( 353 SurfaceHolder holder, int format, int width, int height) { 354 Log.d(TAG, "width: " + width + ", height: " + height); 355 } 356 357 @Override 358 public void surfaceDestroyed(SurfaceHolder holder) { 359 Log.d(TAG, "ICrosvmAndroidDisplayService.removeSurface()"); 360 runWithDisplayService( 361 (service) -> service.removeSurface(false /* forCursor */)); 362 } 363 }); 364 cursorSurfaceView.getHolder().setFormat(PixelFormat.RGBA_8888); 365 cursorSurfaceView 366 .getHolder() 367 .addCallback( 368 new SurfaceHolder.Callback() { 369 @Override 370 public void surfaceCreated(SurfaceHolder holder) { 371 try { 372 ParcelFileDescriptor[] pfds = 373 ParcelFileDescriptor.createSocketPair(); 374 mExecutorService.execute( 375 new CursorHandler(cursorSurfaceView, pfds[0])); 376 mCursorStream = pfds[0]; 377 runWithDisplayService( 378 (service) -> service.setCursorStream(pfds[1])); 379 } catch (Exception e) { 380 Log.d("TAG", "failed to run cursor stream handler", e); 381 } 382 runWithDisplayService( 383 (service) -> 384 service.setSurface( 385 holder.getSurface(), true /* forCursor */)); 386 } 387 388 @Override 389 public void surfaceChanged( 390 SurfaceHolder holder, int format, int width, int height) { 391 Log.d(TAG, "width: " + width + ", height: " + height); 392 } 393 394 @Override 395 public void surfaceDestroyed(SurfaceHolder holder) { 396 Log.d(TAG, "ICrosvmAndroidDisplayService.removeSurface()"); 397 runWithDisplayService( 398 (service) -> service.removeSurface(true /* forCursor */)); 399 if (mCursorStream != null) { 400 try { 401 mCursorStream.close(); 402 } catch (IOException e) { 403 Log.d(TAG, "failed to close fd", e); 404 } 405 } 406 } 407 }); 408 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 409 410 // Fullscreen: 411 WindowInsetsController windowInsetsController = surfaceView.getWindowInsetsController(); 412 windowInsetsController.setSystemBarsBehavior( 413 WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); 414 windowInsetsController.hide(WindowInsets.Type.systemBars()); 415 } 416 417 @Override onDestroy()418 protected void onDestroy() { 419 super.onDestroy(); 420 if (mExecutorService != null) { 421 mExecutorService.shutdownNow(); 422 } 423 Log.d(TAG, "destroyed"); 424 } 425 426 @Override onWindowFocusChanged(boolean hasFocus)427 public void onWindowFocusChanged(boolean hasFocus) { 428 super.onWindowFocusChanged(hasFocus); 429 if (hasFocus) { 430 SurfaceView surfaceView = findViewById(R.id.surface_view); 431 Log.d(TAG, "requestPointerCapture()"); 432 surfaceView.requestPointerCapture(); 433 } 434 } 435 436 @FunctionalInterface 437 public interface RemoteExceptionCheckedFunction<T> { apply(T t)438 void apply(T t) throws RemoteException; 439 } 440 runWithDisplayService( RemoteExceptionCheckedFunction<ICrosvmAndroidDisplayService> func)441 private void runWithDisplayService( 442 RemoteExceptionCheckedFunction<ICrosvmAndroidDisplayService> func) { 443 IVirtualizationServiceInternal vs = 444 IVirtualizationServiceInternal.Stub.asInterface( 445 ServiceManager.waitForService("android.system.virtualizationservice")); 446 try { 447 Log.d(TAG, "wait for the display service"); 448 ICrosvmAndroidDisplayService service = 449 ICrosvmAndroidDisplayService.Stub.asInterface(vs.waitDisplayService()); 450 assert service != null; 451 func.apply(service); 452 Log.d(TAG, "job done"); 453 } catch (Exception e) { 454 Log.d(TAG, "error", e); 455 } 456 } 457 458 static class CursorHandler implements Runnable { 459 private final SurfaceView mSurfaceView; 460 private final ParcelFileDescriptor mStream; 461 CursorHandler(SurfaceView s, ParcelFileDescriptor stream)462 CursorHandler(SurfaceView s, ParcelFileDescriptor stream) { 463 mSurfaceView = s; 464 mStream = stream; 465 } 466 467 @Override run()468 public void run() { 469 Log.d(TAG, "CursorHandler"); 470 try { 471 ByteBuffer byteBuffer = ByteBuffer.allocate(8 /* (x: u32, y: u32) */); 472 byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 473 while (true) { 474 byteBuffer.clear(); 475 int bytes = 476 IoBridge.read( 477 mStream.getFileDescriptor(), 478 byteBuffer.array(), 479 0, 480 byteBuffer.array().length); 481 float x = (float) (byteBuffer.getInt() & 0xFFFFFFFF); 482 float y = (float) (byteBuffer.getInt() & 0xFFFFFFFF); 483 mSurfaceView.post( 484 () -> { 485 mSurfaceView.setTranslationX(x); 486 mSurfaceView.setTranslationY(y); 487 }); 488 } 489 } catch (IOException e) { 490 Log.e(TAG, e.getMessage()); 491 } 492 } 493 } 494 495 /** Reads data from an input stream and posts it to the output data */ 496 static class Reader implements Runnable { 497 private final String mName; 498 private final InputStream mStream; 499 Reader(String name, InputStream stream)500 Reader(String name, InputStream stream) { 501 mName = name; 502 mStream = stream; 503 } 504 505 @Override run()506 public void run() { 507 try { 508 BufferedReader reader = new BufferedReader(new InputStreamReader(mStream)); 509 String line; 510 while ((line = reader.readLine()) != null && !Thread.interrupted()) { 511 Log.d(TAG, mName + ": " + line); 512 } 513 } catch (IOException e) { 514 Log.e(TAG, "Exception while posting " + mName + " output: " + e.getMessage()); 515 } 516 } 517 } 518 519 private static class CopyStreamTask implements Runnable { 520 private final String mName; 521 private final InputStream mIn; 522 private final OutputStream mOut; 523 CopyStreamTask(String name, InputStream in, OutputStream out)524 CopyStreamTask(String name, InputStream in, OutputStream out) { 525 mName = name; 526 mIn = in; 527 mOut = out; 528 } 529 530 @Override run()531 public void run() { 532 try { 533 byte[] buffer = new byte[2048]; 534 while (!Thread.interrupted()) { 535 int len = mIn.read(buffer); 536 if (len < 0) { 537 break; 538 } 539 mOut.write(buffer, 0, len); 540 } 541 } catch (Exception e) { 542 Log.e(TAG, "Exception while posting " + mName, e); 543 } 544 } 545 } 546 547 private static class LineBufferedOutputStream extends BufferedOutputStream { LineBufferedOutputStream(OutputStream out)548 LineBufferedOutputStream(OutputStream out) { 549 super(out); 550 } 551 552 @Override write(byte[] buf, int off, int len)553 public void write(byte[] buf, int off, int len) throws IOException { 554 super.write(buf, off, len); 555 for (int i = 0; i < len; ++i) { 556 if (buf[off + i] == '\n') { 557 flush(); 558 break; 559 } 560 } 561 } 562 } 563 } 564