• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.example.android.vdmdemo.host;
18 
19 import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
20 
21 import android.annotation.SuppressLint;
22 import android.app.ActivityOptions;
23 import android.companion.virtual.VirtualDeviceManager.VirtualDevice;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.PackageManager;
27 import android.graphics.Point;
28 import android.graphics.PointF;
29 import android.hardware.display.DisplayManager;
30 import android.hardware.display.VirtualDisplay;
31 import android.hardware.display.VirtualDisplayConfig;
32 import android.hardware.input.VirtualDpad;
33 import android.hardware.input.VirtualDpadConfig;
34 import android.hardware.input.VirtualKeyEvent;
35 import android.hardware.input.VirtualKeyboard;
36 import android.hardware.input.VirtualKeyboardConfig;
37 import android.hardware.input.VirtualMouse;
38 import android.hardware.input.VirtualMouseButtonEvent;
39 import android.hardware.input.VirtualMouseConfig;
40 import android.hardware.input.VirtualMouseRelativeEvent;
41 import android.hardware.input.VirtualMouseScrollEvent;
42 import android.hardware.input.VirtualNavigationTouchpad;
43 import android.hardware.input.VirtualNavigationTouchpadConfig;
44 import android.hardware.input.VirtualRotaryEncoder;
45 import android.hardware.input.VirtualRotaryEncoderConfig;
46 import android.hardware.input.VirtualRotaryEncoderScrollEvent;
47 import android.hardware.input.VirtualStylus;
48 import android.hardware.input.VirtualStylusButtonEvent;
49 import android.hardware.input.VirtualStylusConfig;
50 import android.hardware.input.VirtualStylusMotionEvent;
51 import android.hardware.input.VirtualTouchEvent;
52 import android.hardware.input.VirtualTouchscreen;
53 import android.hardware.input.VirtualTouchscreenConfig;
54 import android.util.Log;
55 import android.view.Display;
56 import android.view.InputEvent;
57 import android.view.KeyEvent;
58 import android.view.MotionEvent;
59 import android.view.Surface;
60 
61 import androidx.annotation.IntDef;
62 
63 import com.example.android.vdmdemo.common.RemoteEventProto;
64 import com.example.android.vdmdemo.common.RemoteEventProto.BrightnessEvent;
65 import com.example.android.vdmdemo.common.RemoteEventProto.DeviceState;
66 import com.example.android.vdmdemo.common.RemoteEventProto.DisplayRotation;
67 import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
68 import com.example.android.vdmdemo.common.RemoteEventProto.RemoteInputEvent;
69 import com.example.android.vdmdemo.common.RemoteEventProto.RemoteKeyEvent;
70 import com.example.android.vdmdemo.common.RemoteEventProto.RemoteMotionEvent;
71 import com.example.android.vdmdemo.common.RemoteEventProto.StopStreaming;
72 import com.example.android.vdmdemo.common.RemoteIo;
73 import com.example.android.vdmdemo.common.VideoManager;
74 
75 import java.lang.annotation.Retention;
76 import java.lang.annotation.RetentionPolicy;
77 import java.util.Collections;
78 import java.util.Set;
79 import java.util.concurrent.Executors;
80 import java.util.concurrent.atomic.AtomicBoolean;
81 import java.util.function.Consumer;
82 
83 @SuppressLint("NewApi")
84 class RemoteDisplay implements AutoCloseable {
85 
86     private static final String TAG = "VdmHost";
87 
88     private static final int DISPLAY_FPS = 60;
89 
90     private static final int DEFAULT_VIRTUAL_DISPLAY_FLAGS =
91             DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED
92                     | DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
93                     | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
94 
95     private static final float DEFAULT_CLIENT_BRIGHTNESS = 0.3f;
96     private static final float DIM_CLIENT_BRIGHTNESS = 0.15f;
97 
98     static final int DISPLAY_TYPE_APP = 0;
99     static final int DISPLAY_TYPE_HOME = 1;
100     static final int DISPLAY_TYPE_MIRROR = 2;
101     @IntDef(value = {DISPLAY_TYPE_APP, DISPLAY_TYPE_HOME, DISPLAY_TYPE_MIRROR})
102     @Retention(RetentionPolicy.SOURCE)
103     public @interface DisplayType {}
104 
105     private final Context mContext;
106     private final RemoteIo mRemoteIo;
107     private final PreferenceController mPreferenceController;
108     private final Consumer<RemoteEvent> mRemoteEventConsumer = this::processRemoteEvent;
109     private final VirtualDisplay mVirtualDisplay;
110     private final VirtualDpad mDpad;
111     private final int mRemoteDisplayId;
112     private final VirtualDevice mVirtualDevice;
113     private final @DisplayType int mDisplayType;
114     private final AtomicBoolean mClosed = new AtomicBoolean(false);
115     private StatusBar mStatusBar;
116     private int mRotation;
117     private int mWidth;
118     private int mHeight;
119     private int mDpi;
120 
121     private VideoManager mVideoManager;
122     private VirtualTouchscreen mTouchscreen;
123     private VirtualMouse mMouse;
124     private VirtualNavigationTouchpad mNavigationTouchpad;
125     private VirtualKeyboard mKeyboard;
126     private VirtualStylus mStylus;
127     private VirtualRotaryEncoder mRotary;
128 
129     // DisplayManager.DisplayListener#onDisplayChanged along with Display#getState() can also be
130     // used to detect power events instead of using VirtualDisplay.Callback.
131     private final VirtualDisplay.Callback mVirtualDisplayCallback = new VirtualDisplay.Callback() {
132         @Override
133         public void onPaused() {
134             Log.v(TAG, "VirtualDisplay paused");
135             if (mRemoteIo == null) return;
136             mRemoteIo.sendMessage(RemoteEvent.newBuilder()
137                     .setDeviceState(DeviceState.newBuilder().setPowerOn(false))
138                     .build());
139         }
140 
141         @Override
142         public void onResumed() {
143             Log.v(TAG, "VirtualDisplay resumed");
144             if (mRemoteIo == null) return;
145             mRemoteIo.sendMessage(RemoteEvent.newBuilder()
146                     .setDeviceState(DeviceState.newBuilder().setPowerOn(true))
147                     .build());
148         }
149 
150         @Override
151         public void onStopped() {
152             Log.v(TAG, "VirtualDisplay stopped");
153         }
154     };
155 
156     @SuppressLint("WrongConstant")
RemoteDisplay( Context context, int displayId, int width, int height, int dpi, VirtualDevice virtualDevice, RemoteIo remoteIo, @DisplayType int displayType, PreferenceController preferenceController)157     RemoteDisplay(
158             Context context,
159             int displayId,
160             int width,
161             int height,
162             int dpi,
163             VirtualDevice virtualDevice,
164             RemoteIo remoteIo,
165             @DisplayType int displayType,
166             PreferenceController preferenceController) {
167         mContext = context;
168         mRemoteIo = remoteIo;
169         mRemoteDisplayId = displayId;
170         mVirtualDevice = virtualDevice;
171         mDisplayType = displayType;
172         mPreferenceController = preferenceController;
173 
174         setCapabilities(width, height, dpi);
175 
176         int flags = DEFAULT_VIRTUAL_DISPLAY_FLAGS;
177         if (mPreferenceController.getBoolean(R.string.pref_enable_display_rotation)) {
178             flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT;
179         }
180         if (mDisplayType == DISPLAY_TYPE_MIRROR) {
181             flags &= ~DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
182         }
183         if (mContext.checkCallingOrSelfPermission(ADD_TRUSTED_DISPLAY)
184                 == PackageManager.PERMISSION_DENIED) {
185             flags &= ~DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
186         }
187 
188         Set<String> displayCategories;
189         if (mPreferenceController.getBoolean(R.string.pref_enable_display_category)) {
190             displayCategories = Set.of(context.getString(R.string.display_category));
191         } else {
192             displayCategories = Collections.emptySet();
193         }
194 
195         VirtualDisplayConfig.Builder virtualDisplayBuilder =
196                 new VirtualDisplayConfig.Builder(
197                                 "VirtualDisplay" + mRemoteDisplayId, mWidth, mHeight, mDpi)
198                         .setDisplayCategories(displayCategories)
199                         .setFlags(flags);
200 
201         if (mPreferenceController.getBoolean(R.string.pref_enable_client_brightness)) {
202             virtualDisplayBuilder
203                     .setDefaultBrightness(DEFAULT_CLIENT_BRIGHTNESS)
204                     .setDimBrightness(DIM_CLIENT_BRIGHTNESS)
205                     .setBrightnessListener(
206                             Executors.newSingleThreadExecutor(), this::onBrightnessChanged);
207         } else if (mRemoteIo != null) {
208             mRemoteIo.sendMessage(RemoteEvent.newBuilder()
209                     .setBrightnessEvent(BrightnessEvent.newBuilder().setBrightness(-1f))
210                     .build());
211         }
212 
213         if (mDisplayType == DISPLAY_TYPE_HOME) {
214             virtualDisplayBuilder = VdmCompat.setHomeSupported(virtualDisplayBuilder, flags);
215         }
216 
217         mVirtualDisplay =
218                 virtualDevice.createVirtualDisplay(
219                         virtualDisplayBuilder.build(),
220                         Runnable::run,
221                         mVirtualDisplayCallback);
222 
223         VdmCompat.setDisplayImePolicy(
224                 mVirtualDevice,
225                 getDisplayId(),
226                 mPreferenceController.getInt(R.string.pref_display_ime_policy));
227 
228         mDpad =
229                 virtualDevice.createVirtualDpad(
230                         new VirtualDpadConfig.Builder()
231                                 .setAssociatedDisplayId(mVirtualDisplay.getDisplay().getDisplayId())
232                                 .setInputDeviceName("vdmdemo-dpad" + mRemoteDisplayId)
233                                 .build());
234         mKeyboard =
235                 mVirtualDevice.createVirtualKeyboard(
236                         new VirtualKeyboardConfig.Builder()
237                                 .setInputDeviceName(
238                                         "vdmdemo-keyboard" + mRemoteDisplayId)
239                                 .setAssociatedDisplayId(getDisplayId())
240                                 .build());
241 
242         if (mRemoteIo != null) {
243             mRemoteIo.addMessageConsumer(mRemoteEventConsumer);
244         }
245 
246         reset();
247     }
248 
setSurface(Surface surface)249     void setSurface(Surface surface) {
250         mVirtualDisplay.setSurface(surface);
251     }
252 
reset(int width, int height, int dpi)253     void reset(int width, int height, int dpi) {
254         setCapabilities(width, height, dpi);
255         mVirtualDisplay.resize(mWidth, mHeight, mDpi);
256         reset();
257     }
258 
reset()259     private void reset() {
260         if (mVideoManager != null) {
261             mVideoManager.stop();
262         }
263         if (mRemoteIo != null) {
264             mVideoManager = VideoManager.createDisplayEncoder(mRemoteDisplayId, mRemoteIo,
265                     mPreferenceController.getBoolean(R.string.pref_record_encoder_output));
266             Surface surface = mVideoManager.createInputSurface(mWidth, mHeight, DISPLAY_FPS);
267             mVirtualDisplay.setSurface(surface);
268         }
269 
270         mRotation = mVirtualDisplay.getDisplay().getRotation();
271 
272         if (mPreferenceController.getBoolean(R.string.pref_enable_custom_status_bar)
273                 && mDisplayType != DISPLAY_TYPE_MIRROR) {
274             // Custom status bar cannot be shown on mirror displays. Also, it needs to be recreated
275             // whenever the dimensions of the display change.
276             final Context displayContext =
277                     mContext.createDisplayContext(mVirtualDisplay.getDisplay());
278             mContext.getMainExecutor().execute(() -> {
279                 if (mStatusBar != null) {
280                     mStatusBar.destroy(displayContext);
281                 }
282                 mStatusBar = StatusBar.create(displayContext);
283             });
284         }
285 
286         if (mTouchscreen != null) {
287             mTouchscreen.close();
288         }
289         if (mStylus != null) {
290             mStylus.close();
291         }
292         mTouchscreen =
293                 mVirtualDevice.createVirtualTouchscreen(
294                         new VirtualTouchscreenConfig.Builder(mWidth, mHeight)
295                                 .setAssociatedDisplayId(mVirtualDisplay.getDisplay().getDisplayId())
296                                 .setInputDeviceName("vdmdemo-touchscreen" + mRemoteDisplayId)
297                                 .build());
298 
299         if (mVideoManager != null) {
300             mVideoManager.startEncoding();
301         }
302     }
303 
setCapabilities(int width, int height, int dpi)304     private void setCapabilities(int width, int height, int dpi) {
305         mWidth = width;
306         mHeight = height;
307         mDpi = dpi;
308 
309         if (mRemoteIo != null) {
310             // Video encoder needs round dimensions...
311             mHeight -= mHeight % 10;
312             mWidth -= mWidth % 10;
313         }
314     }
315 
launchIntent(Intent intent)316     void launchIntent(Intent intent) {
317         mContext.startActivity(
318                 intent, ActivityOptions.makeBasic().setLaunchDisplayId(getDisplayId()).toBundle());
319     }
320 
getRemoteDisplayId()321     int getRemoteDisplayId() {
322         return mRemoteDisplayId;
323     }
324 
getDisplayId()325     int getDisplayId() {
326         return mVirtualDisplay.getDisplay().getDisplayId();
327     }
328 
getDisplaySize()329     PointF getDisplaySize() {
330         return new PointF(mWidth, mHeight);
331     }
332 
getWidth()333     int getWidth() {
334         return mWidth;
335     }
336 
getHeight()337     int getHeight() {
338         return mHeight;
339     }
340 
onDisplayChanged()341     void onDisplayChanged() {
342         if (mRotation != mVirtualDisplay.getDisplay().getRotation()) {
343             mRotation = mVirtualDisplay.getDisplay().getRotation();
344             if (mRemoteIo == null) return;
345             int rotationDegrees = displayRotationToDegrees(mRotation);
346             Log.v(TAG, "Notify client for rotation event: " + rotationDegrees);
347             mRemoteIo.sendMessage(
348                     RemoteEvent.newBuilder()
349                             .setDisplayId(getRemoteDisplayId())
350                             .setDisplayRotation(
351                                     DisplayRotation.newBuilder()
352                                             .setRotationDegrees(rotationDegrees))
353                             .build());
354         }
355     }
356 
onBrightnessChanged(float brightness)357     private void onBrightnessChanged(float brightness) {
358         Log.v(TAG, "VirtualDisplay brightness changed to " + brightness);
359         if (mRemoteIo == null) return;
360         mRemoteIo.sendMessage(RemoteEvent.newBuilder()
361                 .setBrightnessEvent(BrightnessEvent.newBuilder().setBrightness(brightness))
362                 .build());
363     }
364 
processRemoteEvent(RemoteEvent event)365     void processRemoteEvent(RemoteEvent event) {
366         if (event.getDisplayId() != mRemoteDisplayId) {
367             return;
368         }
369         if (event.hasHomeEvent()) {
370             goHome();
371         } else if (event.hasInputEvent()) {
372             processInputEvent(event.getInputEvent());
373         } else if (event.hasDisplayRotation()) {
374             int rotation = mVirtualDisplay.getDisplay().getRotation();
375             // Change the rotation of the display. The rotation is a Surface rotation and has
376             // only 4 possible values.
377             rotation += 1;
378             rotation %= 4;
379             mVirtualDisplay.setRotation(rotation);
380         } else if (event.hasStopStreaming() && event.getStopStreaming().getPause()) {
381             if (mVideoManager != null) {
382                 mVideoManager.stop();
383                 mVideoManager = null;
384             }
385         }
386     }
387 
goHome()388     void goHome() {
389         if (mDisplayType != DISPLAY_TYPE_HOME && mDisplayType != DISPLAY_TYPE_MIRROR) {
390             return;
391         }
392         Intent homeIntent = new Intent(Intent.ACTION_MAIN);
393         homeIntent.addCategory(Intent.CATEGORY_HOME);
394         homeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
395         int targetDisplayId =
396                 mDisplayType == DISPLAY_TYPE_MIRROR ? Display.DEFAULT_DISPLAY : getDisplayId();
397         mContext.startActivity(
398                 homeIntent,
399                 ActivityOptions.makeBasic().setLaunchDisplayId(targetDisplayId).toBundle());
400     }
401 
sendBack()402     void sendBack() {
403         for (int action : new int[]{VirtualKeyEvent.ACTION_DOWN, VirtualKeyEvent.ACTION_UP}) {
404             mDpad.sendKeyEvent(new VirtualKeyEvent.Builder()
405                     .setKeyCode(KeyEvent.KEYCODE_BACK)
406                     .setAction(action)
407                     .build());
408         }
409     }
410 
processInputEvent(RemoteInputEvent inputEvent)411     private void processInputEvent(RemoteInputEvent inputEvent) {
412         switch (inputEvent.getDeviceType()) {
413             case DEVICE_TYPE_NONE:
414                 Log.e(TAG, "Received no input device type");
415                 break;
416             case DEVICE_TYPE_DPAD:
417                 mDpad.sendKeyEvent(remoteEventToVirtualKeyEvent(inputEvent));
418                 break;
419             case DEVICE_TYPE_NAVIGATION_TOUCHPAD:
420                 processNavigationTouchpadEvent(remoteEventToVirtualTouchEvent(inputEvent));
421                 break;
422             case DEVICE_TYPE_MOUSE:
423                 processMouseEvent(inputEvent);
424                 break;
425             case DEVICE_TYPE_TOUCHSCREEN:
426                 mTouchscreen.sendTouchEvent(remoteEventToVirtualTouchEvent(inputEvent));
427                 break;
428             case DEVICE_TYPE_KEYBOARD:
429                 mKeyboard.sendKeyEvent(remoteEventToVirtualKeyEvent(inputEvent));
430                 break;
431             case DEVICE_TYPE_ROTARY_ENCODER:
432                 processRotaryEvent(remoteEventToVirtualRotaryEncoderEvent(inputEvent));
433                 break;
434             default:
435                 Log.e(TAG, "processInputEvent got an invalid input device type: "
436                         + inputEvent.getDeviceType().getNumber());
437                 break;
438         }
439     }
440 
processInputEvent(RemoteEventProto.InputDeviceType deviceType, InputEvent event)441     void processInputEvent(RemoteEventProto.InputDeviceType deviceType, InputEvent event) {
442         switch (deviceType) {
443             case DEVICE_TYPE_DPAD:
444                 mDpad.sendKeyEvent(keyEventToVirtualKeyEvent((KeyEvent) event));
445                 break;
446             case DEVICE_TYPE_NAVIGATION_TOUCHPAD:
447                 processNavigationTouchpadEvent(motionEventToVirtualTouchEvent((MotionEvent) event));
448                 break;
449             case DEVICE_TYPE_KEYBOARD:
450                 mKeyboard.sendKeyEvent(keyEventToVirtualKeyEvent((KeyEvent) event));
451                 break;
452             case DEVICE_TYPE_ROTARY_ENCODER:
453                 processRotaryEvent(motionEventToVirtualRotaryEncoderEvent((MotionEvent) event));
454                 break;
455             case DEVICE_TYPE_MOUSE:
456                 processVirtualMouseEvent(motionEventToVirtualMouseEvent((MotionEvent) event));
457                 break;
458             case DEVICE_TYPE_TOUCHSCREEN:
459                 mTouchscreen.sendTouchEvent(motionEventToVirtualTouchEvent((MotionEvent) event));
460                 break;
461             default:
462                 Log.e(TAG, "processInputEvent got an invalid input device type: "
463                         + deviceType.getNumber());
464                 break;
465         }
466     }
467 
processNavigationTouchpadEvent(VirtualTouchEvent event)468     private void processNavigationTouchpadEvent(VirtualTouchEvent event) {
469         if (mNavigationTouchpad == null) {
470             // Any arbitrarily big enough nav touchpad would work.
471             Point displaySize = new Point(5000, 5000);
472             mNavigationTouchpad =
473                     mVirtualDevice.createVirtualNavigationTouchpad(
474                             new VirtualNavigationTouchpadConfig.Builder(
475                                     displaySize.x, displaySize.y)
476                                     .setAssociatedDisplayId(getDisplayId())
477                                     .setInputDeviceName(
478                                             "vdmdemo-navtouchpad" + mRemoteDisplayId)
479                                     .build());
480         }
481         mNavigationTouchpad.sendTouchEvent(event);
482 
483     }
484 
processVirtualMouseEvent(Object mouseEvent)485     void processVirtualMouseEvent(Object mouseEvent) {
486         if (!createMouseIfNeeded()) {
487             return;
488         }
489         if (mouseEvent instanceof VirtualMouseButtonEvent) {
490             mMouse.sendButtonEvent((VirtualMouseButtonEvent) mouseEvent);
491         } else if (mouseEvent instanceof VirtualMouseScrollEvent) {
492             mMouse.sendScrollEvent((VirtualMouseScrollEvent) mouseEvent);
493         } else if (mouseEvent instanceof VirtualMouseRelativeEvent) {
494             mMouse.sendRelativeEvent((VirtualMouseRelativeEvent) mouseEvent);
495         }
496     }
497 
processVirtualStylusEvent(Object stylusEvent)498     void processVirtualStylusEvent(Object stylusEvent) {
499         if (mStylus == null) {
500             mStylus = mVirtualDevice.createVirtualStylus(
501                     new VirtualStylusConfig.Builder(mWidth, mHeight)
502                             .setAssociatedDisplayId(getDisplayId())
503                             .setInputDeviceName("vdmdemo-stylus" + mRemoteDisplayId)
504                             .build());
505         }
506         if (stylusEvent instanceof VirtualStylusMotionEvent) {
507             mStylus.sendMotionEvent((VirtualStylusMotionEvent) stylusEvent);
508         } else if (stylusEvent instanceof VirtualStylusButtonEvent) {
509             mStylus.sendButtonEvent((VirtualStylusButtonEvent) stylusEvent);
510         }
511     }
512 
processRotaryEvent(VirtualRotaryEncoderScrollEvent rotaryEvent)513     void processRotaryEvent(VirtualRotaryEncoderScrollEvent rotaryEvent) {
514         if (mRotary == null) {
515             mRotary = mVirtualDevice.createVirtualRotaryEncoder(
516                     new VirtualRotaryEncoderConfig.Builder()
517                             .setAssociatedDisplayId(getDisplayId())
518                             .setInputDeviceName("vdmdemo-rotary" + mRemoteDisplayId)
519                             .build());
520         }
521         mRotary.sendScrollEvent(rotaryEvent);
522     }
523 
processMouseEvent(RemoteInputEvent inputEvent)524     private void processMouseEvent(RemoteInputEvent inputEvent) {
525         if (!createMouseIfNeeded()) {
526             return;
527         }
528         if (inputEvent.hasMouseButtonEvent()) {
529             mMouse.sendButtonEvent(
530                     new VirtualMouseButtonEvent.Builder()
531                             .setButtonCode(inputEvent.getMouseButtonEvent().getKeyCode())
532                             .setAction(inputEvent.getMouseButtonEvent().getAction())
533                             .build());
534         } else if (inputEvent.hasMouseScrollEvent()) {
535             mMouse.sendScrollEvent(
536                     new VirtualMouseScrollEvent.Builder()
537                             .setXAxisMovement(inputEvent.getMouseScrollEvent().getX())
538                             .setYAxisMovement(inputEvent.getMouseScrollEvent().getY())
539                             .build());
540         } else if (inputEvent.hasMouseRelativeEvent()) {
541             PointF cursorPosition = mMouse.getCursorPosition();
542             mMouse.sendRelativeEvent(
543                     new VirtualMouseRelativeEvent.Builder()
544                             .setRelativeX(
545                                     inputEvent.getMouseRelativeEvent().getX() - cursorPosition.x)
546                             .setRelativeY(
547                                     inputEvent.getMouseRelativeEvent().getY() - cursorPosition.y)
548                             .build());
549         } else {
550             Log.e(TAG, "Received an invalid mouse event");
551         }
552     }
553 
createMouseIfNeeded()554     private boolean createMouseIfNeeded() {
555         if (mMouse == null && VdmCompat.canCreateVirtualMouse(mContext)) {
556             mMouse =
557                     mVirtualDevice.createVirtualMouse(
558                             new VirtualMouseConfig.Builder()
559                                     .setAssociatedDisplayId(getDisplayId())
560                                     .setInputDeviceName("vdmdemo-mouse" + mRemoteDisplayId)
561                                     .build());
562         }
563         return mMouse != null;
564     }
565 
getVirtualTouchEventAction(int action)566     private static int getVirtualTouchEventAction(int action) {
567         return switch (action) {
568             case MotionEvent.ACTION_POINTER_DOWN -> VirtualTouchEvent.ACTION_DOWN;
569             case MotionEvent.ACTION_POINTER_UP -> VirtualTouchEvent.ACTION_UP;
570             default -> action;
571         };
572     }
573 
getVirtualTouchEventToolType(int action)574     private static int getVirtualTouchEventToolType(int action) {
575         return switch (action) {
576             case MotionEvent.ACTION_CANCEL -> VirtualTouchEvent.TOOL_TYPE_PALM;
577             default -> VirtualTouchEvent.TOOL_TYPE_FINGER;
578         };
579     }
580 
581     // Surface rotation is in opposite direction to display rotation.
582     // See https://developer.android.com/reference/android/view/Display?hl=en#getRotation()
583     private static int displayRotationToDegrees(int displayRotation) {
584         return switch (displayRotation) {
585             case Surface.ROTATION_90 -> -90;
586             case Surface.ROTATION_180 -> -180;
587             case Surface.ROTATION_270 -> -270;
588             default -> 0;
589         };
590     }
591 
592     private static VirtualKeyEvent remoteEventToVirtualKeyEvent(RemoteInputEvent event) {
593         RemoteKeyEvent keyEvent = event.getKeyEvent();
594         return new VirtualKeyEvent.Builder()
595                 .setEventTimeNanos((long) (event.getTimestampMs() * 1e6))
596                 .setKeyCode(keyEvent.getKeyCode())
597                 .setAction(keyEvent.getAction())
598                 .build();
599     }
600 
601     private static VirtualKeyEvent keyEventToVirtualKeyEvent(KeyEvent keyEvent) {
602         return new VirtualKeyEvent.Builder()
603                 .setEventTimeNanos((long) (keyEvent.getEventTime() * 1e6))
604                 .setKeyCode(keyEvent.getKeyCode())
605                 .setAction(keyEvent.getAction())
606                 .build();
607     }
608 
609     private static VirtualTouchEvent remoteEventToVirtualTouchEvent(RemoteInputEvent event) {
610         RemoteMotionEvent motionEvent = event.getTouchEvent();
611         return new VirtualTouchEvent.Builder()
612                 .setEventTimeNanos((long) (event.getTimestampMs() * 1e6))
613                 .setPointerId(motionEvent.getPointerId())
614                 .setAction(getVirtualTouchEventAction(motionEvent.getAction()))
615                 .setPressure(motionEvent.getPressure() * 255f)
616                 .setToolType(getVirtualTouchEventToolType(motionEvent.getAction()))
617                 .setX(motionEvent.getX())
618                 .setY(motionEvent.getY())
619                 .build();
620     }
621 
622     private static VirtualTouchEvent motionEventToVirtualTouchEvent(MotionEvent motionEvent) {
623         return new VirtualTouchEvent.Builder()
624                 .setEventTimeNanos((long) (motionEvent.getEventTime() * 1e6))
625                 .setPointerId(1)
626                 .setAction(getVirtualTouchEventAction(motionEvent.getAction()))
627                 .setPressure(motionEvent.getPressure() * 255f)
628                 .setToolType(getVirtualTouchEventToolType(motionEvent.getAction()))
629                 .setX(motionEvent.getX())
630                 .setY(motionEvent.getY())
631                 .build();
632     }
633 
634     private static Object motionEventToVirtualMouseEvent(MotionEvent event) {
635         switch (event.getAction()) {
636             case MotionEvent.ACTION_BUTTON_PRESS:
637             case MotionEvent.ACTION_BUTTON_RELEASE:
638                 return new VirtualMouseButtonEvent.Builder()
639                         .setEventTimeNanos((long) (event.getEventTime() * 1e6))
640                         .setButtonCode(event.getActionButton())
641                         .setAction(event.getAction())
642                         .build();
643             case MotionEvent.ACTION_HOVER_ENTER:
644             case MotionEvent.ACTION_HOVER_EXIT:
645             case MotionEvent.ACTION_HOVER_MOVE:
646                 return new VirtualMouseRelativeEvent.Builder()
647                         .setEventTimeNanos((long) (event.getEventTime() * 1e6))
648                         .setRelativeX(event.getX())
649                         .setRelativeY(event.getY())
650                         .build();
651             case MotionEvent.ACTION_SCROLL:
652                 float scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
653                 float scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
654                 return new VirtualMouseScrollEvent.Builder()
655                         .setEventTimeNanos((long) (event.getEventTime() * 1e6))
656                         .setXAxisMovement(InputController.clampMouseScroll(scrollX))
657                         .setYAxisMovement(InputController.clampMouseScroll(scrollY))
658                         .build();
659             default:
660                 return null;
661         }
662     }
663 
664     private static VirtualRotaryEncoderScrollEvent remoteEventToVirtualRotaryEncoderEvent(
665             RemoteInputEvent event) {
666         return new VirtualRotaryEncoderScrollEvent.Builder()
667                 .setEventTimeNanos((long) (event.getTimestampMs() * 1e6))
668                 .setScrollAmount(event.getMouseScrollEvent().getX())
669                 .build();
670     }
671 
672     private static VirtualRotaryEncoderScrollEvent motionEventToVirtualRotaryEncoderEvent(
673             MotionEvent motionEvent) {
674         return new VirtualRotaryEncoderScrollEvent.Builder()
675                 .setEventTimeNanos((long) (motionEvent.getEventTime() * 1e6))
676                 .setScrollAmount(motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL))
677                 .build();
678     }
679 
680     @Override
681     public void close() {
682         if (mClosed.getAndSet(true)) { // Prevent double closure.
683             return;
684         }
685         if (mRemoteIo != null) {
686             mRemoteIo.sendMessage(
687                     RemoteEvent.newBuilder()
688                             .setDisplayId(getRemoteDisplayId())
689                             .setStopStreaming(StopStreaming.newBuilder().setPause(false))
690                             .build());
691             mRemoteIo.removeMessageConsumer(mRemoteEventConsumer);
692         }
693         mDpad.close();
694         mTouchscreen.close();
695         mKeyboard.close();
696         if (mRotary != null) {
697             mRotary.close();
698         }
699         if (mStylus != null) {
700             mStylus.close();
701         }
702         if (mMouse != null) {
703             mMouse.close();
704         }
705         if (mNavigationTouchpad != null) {
706             mNavigationTouchpad.close();
707         }
708         mVirtualDisplay.release();
709         if (mVideoManager != null) {
710             mVideoManager.stop();
711             mVideoManager = null;
712         }
713     }
714 }
715