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