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