Inject mouse events from VDM Host

Test: manual
Bug: 317170179
Change-Id: I4c26aa008a0a36675806a2d944e28ae0995e41a5
diff --git a/samples/VirtualDeviceManager/README.md b/samples/VirtualDeviceManager/README.md
index 1bb2032..a40fb0a 100644
--- a/samples/VirtualDeviceManager/README.md
+++ b/samples/VirtualDeviceManager/README.md
@@ -177,6 +177,14 @@
 display is indicated by the frame around its header whenever there are more than
 one displays. The display focus is based on user interaction.
 
+Each input screen has a "Back", "Home" and "Forward" buttons.
+
+-   **Touchpad** shows an on-screen touchpad for injecting mouse events into
+    the focused display.
+
+-   **Remote** allows the host device to act as a pointer that controls the
+    mouse movement on the focused display.
+
 -   **Navigation** shows an on-screen D-Pad and touchpad for navigating the
     activity on the focused display.
 
diff --git a/samples/VirtualDeviceManager/host/res/drawable/home.xml b/samples/VirtualDeviceManager/host/res/drawable/home.xml
new file mode 100644
index 0000000..ad899ed
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/drawable/home.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M160,840L160,360L480,120L800,360L800,840L560,840L560,560L400,560L400,840L160,840Z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/host/res/drawable/input_mouse.xml b/samples/VirtualDeviceManager/host/res/drawable/input_mouse.xml
new file mode 100644
index 0000000..7026842
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/drawable/input_mouse.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M660,880Q569,880 504.5,815.5Q440,751 440,660L440,600L880,600L880,660Q880,751 816,815.5Q752,880 660,880ZM160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,336Q837,290 780,265Q723,240 660,240Q535,240 447.5,327.5Q360,415 360,540L360,660Q360,697 368.5,732.5Q377,768 394,800L160,800ZM440,520Q443,445 494,390.5Q545,336 620,323L620,520L440,520ZM700,520L700,323Q775,336 826,390.5Q877,445 880,520L700,520Z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/host/res/drawable/input_remote.xml b/samples/VirtualDeviceManager/host/res/drawable/input_remote.xml
new file mode 100644
index 0000000..9f965e9
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/drawable/input_remote.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M480,280Q455,280 437.5,262.5Q420,245 420,220Q420,195 437.5,177.5Q455,160 480,160Q505,160 522.5,177.5Q540,195 540,220Q540,245 522.5,262.5Q505,280 480,280ZM480,920Q397,920 338.5,861.5Q280,803 280,720L280,240Q280,157 338.5,98.5Q397,40 480,40Q563,40 621.5,98.5Q680,157 680,240L680,720Q680,803 621.5,861.5Q563,920 480,920ZM480,320Q522,320 551,291Q580,262 580,220Q580,178 551,149Q522,120 480,120Q438,120 409,149Q380,178 380,220Q380,262 409,291Q438,320 480,320ZM420,640Q437,640 448.5,628.5Q460,617 460,600Q460,583 448.5,571.5Q437,560 420,560Q403,560 391.5,571.5Q380,583 380,600Q380,617 391.5,628.5Q403,640 420,640ZM420,520Q437,520 448.5,508.5Q460,497 460,480Q460,463 448.5,451.5Q437,440 420,440Q403,440 391.5,451.5Q380,463 380,480Q380,497 391.5,508.5Q403,520 420,520ZM540,520Q557,520 568.5,508.5Q580,497 580,480Q580,463 568.5,451.5Q557,440 540,440Q523,440 511.5,451.5Q500,463 500,480Q500,497 511.5,508.5Q523,520 540,520ZM540,640Q557,640 568.5,628.5Q580,617 580,600Q580,583 568.5,571.5Q557,560 540,560Q523,560 511.5,571.5Q500,583 500,600Q500,617 511.5,628.5Q523,640 540,640ZM420,760Q437,760 448.5,748.5Q460,737 460,720Q460,703 448.5,691.5Q437,680 420,680Q403,680 391.5,691.5Q380,703 380,720Q380,737 391.5,748.5Q403,760 420,760ZM540,760Q557,760 568.5,748.5Q580,737 580,720Q580,703 568.5,691.5Q557,680 540,680Q523,680 511.5,691.5Q500,703 500,720Q500,737 511.5,748.5Q523,760 540,760Z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/host/res/layout/activity_input.xml b/samples/VirtualDeviceManager/host/res/layout/activity_input.xml
index 0e34f08..618c3f7 100644
--- a/samples/VirtualDeviceManager/host/res/layout/activity_input.xml
+++ b/samples/VirtualDeviceManager/host/res/layout/activity_input.xml
@@ -21,6 +21,38 @@
         android:layout_height="0dp"
         android:layout_weight="1"/>
 
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:orientation="horizontal">
+
+        <ImageButton
+            android:id="@+id/button_back"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:contentDescription="@string/button_back"
+            android:src="@drawable/dpad_left" />
+
+        <ImageButton
+            android:id="@+id/button_home"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:contentDescription="@string/button_home"
+            android:src="@drawable/home" />
+
+        <ImageButton
+            android:id="@+id/button_forward"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:contentDescription="@string/button_forward"
+            android:src="@drawable/dpad_right" />
+
+    </LinearLayout>
+
     <com.google.android.material.bottomnavigation.BottomNavigationView
         android:id="@+id/bottom_nav"
         android:layout_width="match_parent"
diff --git a/samples/VirtualDeviceManager/host/res/layout/fragment_input_mouse.xml b/samples/VirtualDeviceManager/host/res/layout/fragment_input_mouse.xml
new file mode 100644
index 0000000..8eb7bb4
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/layout/fragment_input_mouse.xml
@@ -0,0 +1,7 @@
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/touchpad"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_margin="5dp"
+    android:gravity="center"
+    android:background="?attr/colorButtonNormal" />
diff --git a/samples/VirtualDeviceManager/host/res/menu/input.xml b/samples/VirtualDeviceManager/host/res/menu/input.xml
index 88d8e83..d394b8c 100644
--- a/samples/VirtualDeviceManager/host/res/menu/input.xml
+++ b/samples/VirtualDeviceManager/host/res/menu/input.xml
@@ -2,6 +2,14 @@
 <!-- LINT.IfChange -->
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
     <item
+        android:id="@+id/mouse"
+        android:icon="@drawable/input_mouse"
+        android:title="@string/touchpad" />
+    <item
+        android:id="@+id/remote"
+        android:icon="@drawable/input_remote"
+        android:title="@string/remote_control" />
+    <item
         android:id="@+id/navigation"
         android:icon="@drawable/input_navigation"
         android:title="@string/navigation" />
diff --git a/samples/VirtualDeviceManager/host/res/values/strings.xml b/samples/VirtualDeviceManager/host/res/values/strings.xml
index 32e9f79..1d02157 100644
--- a/samples/VirtualDeviceManager/host/res/values/strings.xml
+++ b/samples/VirtualDeviceManager/host/res/values/strings.xml
@@ -6,8 +6,25 @@
     <string name="app_icon_description" translatable="false">Application Icon</string>
     <string name="settings" translatable="false">Settings</string>
     <string name="input" translatable="false">Input</string>
+    <string name="touchpad" translatable="false">Touchpad</string>
+    <string name="remote_control" translatable="false">Remote</string>
     <string name="navigation" translatable="false">Navigation</string>
     <string name="keyboard" translatable="false">Keyboard</string>
+    <string name="touchpad_label" translatable="false">
+        Swipe to move the pointer\n\n
+        Tap to select (left click)\n\n
+        Long press for options (right click)\n\n
+        Swipe with 2 fingers to scroll
+    </string>
+    <string name="remote_control_label" translatable="false">
+        Move the device to move the pointer\n\n
+        Tap to select (left click)\n\n
+        Long press for options (right click)\n\n
+        Swipe to scroll
+    </string>
+    <string name="button_back" translatable="false">Back</string>
+    <string name="button_forward" translatable="false">Forward</string>
+    <string name="button_home" translatable="false">Home</string>
 
     <string name="pref_device_profile" translatable="false">device_profile</string>
     <string name="pref_hide_from_recents" translatable="false">hide_from_recents</string>
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/InputActivity.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/InputActivity.java
index a7dcbdb..c330e30 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/InputActivity.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/InputActivity.java
@@ -18,12 +18,14 @@
 
 import android.os.Bundle;
 import android.view.KeyEvent;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.inputmethod.InputMethodManager;
 
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.widget.Toolbar;
 import androidx.fragment.app.Fragment;
+import androidx.preference.PreferenceManager;
 
 import com.example.android.vdmdemo.common.DpadFragment;
 import com.example.android.vdmdemo.common.NavTouchpadFragment;
@@ -43,6 +45,8 @@
     @Inject InputController mInputController;
     @Inject PreferenceController mPreferenceController;
 
+    boolean mOriginalShowPointerIconPreference;
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -53,6 +57,11 @@
         toolbar.setNavigationOnClickListener(v -> finish());
         setTitle(getTitle() + " " + getString(R.string.input));
 
+        mOriginalShowPointerIconPreference =
+                mPreferenceController.getBoolean(R.string.pref_show_pointer_icon);
+
+        Fragment touchpadFragment = new MouseFragment.TouchpadFragment();
+        Fragment remoteFragment = new MouseFragment.RemoteFragment();
         Fragment navigationFragment = new NavigationFragment();
         Fragment keyboardFragment = new Fragment();
 
@@ -60,8 +69,20 @@
         bottomNavigationView.setOnItemSelectedListener(item -> {
             Fragment fragment;
             switch (item.getItemId()) {
-                case R.id.navigation -> fragment = navigationFragment;
+                case R.id.mouse -> {
+                    setShowPointerIcon(true);
+                    fragment = touchpadFragment;
+                }
+                case R.id.remote -> {
+                    setShowPointerIcon(true);
+                    fragment = remoteFragment;
+                }
+                case R.id.navigation -> {
+                    setShowPointerIcon(mOriginalShowPointerIconPreference);
+                    fragment = navigationFragment;
+                }
                 case R.id.keyboard -> {
+                    setShowPointerIcon(mOriginalShowPointerIconPreference);
                     fragment = keyboardFragment;
                     getSystemService(InputMethodManager.class)
                             .showSoftInput(getWindow().getDecorView(), 0);
@@ -76,7 +97,27 @@
                     .commit();
             return true;
         });
-        bottomNavigationView.setSelectedItemId(R.id.navigation);
+        bottomNavigationView.setSelectedItemId(R.id.mouse);
+
+        requireViewById(R.id.button_back).setOnClickListener(
+                v -> mInputController.sendMouseButtonEvent(MotionEvent.BUTTON_BACK));
+        requireViewById(R.id.button_forward).setOnClickListener(
+                v -> mInputController.sendMouseButtonEvent(MotionEvent.BUTTON_FORWARD));
+        requireViewById(R.id.button_home).setOnClickListener(
+                v -> mInputController.sendHomeToFocusedDisplay());
+    }
+
+    private void setShowPointerIcon(boolean show) {
+        PreferenceManager.getDefaultSharedPreferences(this)
+                .edit()
+                .putBoolean(getString(R.string.pref_show_pointer_icon), show)
+                .commit();
+    }
+
+    @Override
+    public void onDestroy() {
+        setShowPointerIcon(mOriginalShowPointerIconPreference);
+        super.onDestroy();
     }
 
     @Override
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/InputController.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/InputController.java
index 5755225..605df06 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/InputController.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/InputController.java
@@ -16,14 +16,21 @@
 
 package com.example.android.vdmdemo.host;
 
+import android.graphics.PointF;
+import android.hardware.input.VirtualMouseButtonEvent;
+import android.hardware.input.VirtualMouseRelativeEvent;
+import android.hardware.input.VirtualMouseScrollEvent;
 import android.util.Log;
 import android.view.Display;
 import android.view.InputEvent;
+import android.view.MotionEvent;
 
 import androidx.annotation.GuardedBy;
 
 import com.example.android.vdmdemo.common.RemoteEventProto.InputDeviceType;
 
+import java.util.Optional;
+
 import javax.inject.Inject;
 import javax.inject.Singleton;
 
@@ -55,16 +62,75 @@
 
     void sendEventToFocusedDisplay(InputDeviceType deviceType, InputEvent inputEvent) {
         synchronized (mLock) {
+            getFocusedDisplayLocked().ifPresent(d -> d.processInputEvent(deviceType, inputEvent));
+        }
+    }
+
+    void sendHomeToFocusedDisplay() {
+        synchronized (mLock) {
+            getFocusedDisplayLocked().ifPresent(d -> d.goHome());
+        }
+    }
+
+    void sendMouseButtonEvent(int button) {
+        for (int action : new int[]{
+                MotionEvent.ACTION_BUTTON_PRESS, MotionEvent.ACTION_BUTTON_RELEASE}) {
+            sendMouseEventToFocusedDisplay(
+                    new VirtualMouseButtonEvent.Builder()
+                            .setButtonCode(button)
+                            .setAction(action)
+                            .build());
+        }
+    }
+
+    void sendMouseRelativeEvent(float x, float y) {
+        sendMouseEventToFocusedDisplay(
+                new VirtualMouseRelativeEvent.Builder()
+                        .setRelativeX(x)
+                        .setRelativeY(y)
+                        .build());
+    }
+
+    void sendMouseScrollEvent(float x, float y) {
+        sendMouseEventToFocusedDisplay(
+                new VirtualMouseScrollEvent.Builder()
+                        .setXAxisMovement(clampMouseScroll(x))
+                        .setYAxisMovement(clampMouseScroll(y))
+                        .build());
+    }
+
+    private void sendMouseEventToFocusedDisplay(Object mouseEvent) {
+        synchronized (mLock) {
+            getFocusedDisplayLocked().ifPresent(d -> d.processVirtualMouseEvent(mouseEvent));
+        }
+    }
+
+    Optional<PointF> getFocusedDisplaySize() {
+        synchronized (mLock) {
+            Optional<RemoteDisplay> display = getFocusedDisplayLocked();
+            return display.isPresent()
+                    ? Optional.of(display.get().getDisplaySize())
+                    : Optional.empty();
+        }
+    }
+
+    @GuardedBy("mLock")
+    private Optional<RemoteDisplay> getFocusedDisplayLocked() {
+        synchronized (mLock) {
             if (mFocusedDisplay == null) {
                 mFocusedDisplay = mDisplayRepository.getDisplayByRemoteId(mFocusedRemoteDisplayId)
                         .orElse(null);
                 if (mFocusedDisplay == null) {
                     Log.e(TAG, "Failed to inject input event, no focused display "
                             + mFocusedRemoteDisplayId);
-                    return;
+                    return Optional.empty();
                 }
             }
-            mFocusedDisplay.processInputEvent(deviceType, inputEvent);
+            return Optional.of(mFocusedDisplay);
         }
     }
+
+    private static float clampMouseScroll(float val) {
+        return Math.max(Math.min(val, 1f), -1f);
+    }
 }
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/MouseFragment.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/MouseFragment.java
new file mode 100644
index 0000000..f05275b
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/MouseFragment.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.vdmdemo.host;
+
+import android.graphics.PointF;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Bundle;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+import androidx.core.view.GestureDetectorCompat;
+import androidx.fragment.app.Fragment;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+import javax.inject.Inject;
+
+/** VDM Host Input fragment for mouse event injection. */
+@AndroidEntryPoint(Fragment.class)
+public abstract class MouseFragment extends Hilt_MouseFragment {
+
+    @Inject
+    InputController mInputController;
+
+    private GestureDetectorCompat mDetector;
+
+    protected GestureListener mGestureListener = new GestureListener();
+    protected int mNumFingers = 0;
+
+
+    public MouseFragment() {
+        super(R.layout.fragment_input_mouse);
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        mDetector = new GestureDetectorCompat(requireContext(), mGestureListener);
+        TextView touchpad = view.requireViewById(R.id.touchpad);
+        touchpad.setText(getViewTextResourceId());
+        touchpad.setOnTouchListener((v, e) -> {
+            mNumFingers = e.getPointerCount();
+            return mDetector.onTouchEvent(e);
+        });
+    }
+
+    protected abstract @StringRes int getViewTextResourceId();
+
+    protected class GestureListener extends GestureDetector.SimpleOnGestureListener {
+
+        private static final float SCROLL_THRESHOLD = 80f;
+        private float mScrollX = 0;
+        private float mScrollY = 0;
+
+        @Override
+        public boolean onDown(@NonNull MotionEvent e) {
+            mScrollX = mScrollY = 0;
+            return true;
+        }
+
+        @Override
+        public boolean onSingleTapUp(@NonNull MotionEvent e) {
+            mInputController.sendMouseButtonEvent(MotionEvent.BUTTON_PRIMARY);
+            return true;
+        }
+
+        @Override
+        public void onLongPress(@NonNull MotionEvent e) {
+            mInputController.sendMouseButtonEvent(MotionEvent.BUTTON_SECONDARY);
+        }
+
+        @Override
+        public boolean onScroll(
+                MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) {
+            mScrollX += distanceX;
+            mScrollY += distanceY;
+            if (Math.abs(mScrollX) > SCROLL_THRESHOLD && Math.abs(distanceY) > SCROLL_THRESHOLD) {
+                mInputController.sendMouseScrollEvent(mScrollX, mScrollY);
+                mScrollX = mScrollY = 0;
+            } else if (Math.abs(mScrollX) > SCROLL_THRESHOLD) {
+                mInputController.sendMouseScrollEvent(mScrollX, 0);
+                mScrollX = 0;
+            } else if (Math.abs(mScrollY) > SCROLL_THRESHOLD) {
+                mInputController.sendMouseScrollEvent(0, mScrollY);
+                mScrollY = 0;
+            }
+            return true;
+        }
+    }
+
+    @AndroidEntryPoint(MouseFragment.class)
+    public static final class TouchpadFragment extends Hilt_MouseFragment_TouchpadFragment {
+
+        public TouchpadFragment() {
+            mGestureListener = new GestureListener() {
+                @Override
+                public boolean onScroll(
+                        MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) {
+                    if (mNumFingers == 1) {
+                        mInputController.sendMouseRelativeEvent(-distanceX, -distanceY);
+                    } else if (mNumFingers == 2) {
+                        return super.onScroll(e1, e2, distanceX, distanceY);
+                    }
+                    return true;
+                }
+            };
+        }
+
+        @Override
+        protected @StringRes int getViewTextResourceId() {
+            return R.string.touchpad_label;
+        }
+    }
+
+
+    @AndroidEntryPoint(MouseFragment.class)
+    public static final class RemoteFragment extends Hilt_MouseFragment_RemoteFragment {
+
+        private static final float SENSOR_EVENT_THRESHOLD = 0.04f;
+        private static final float SENSOR_EVENT_SCALE = 0.025f;
+
+        private SensorManager mSensorManager;
+
+        private final SensorEventListener mSensorEventListener = new SensorEventListener() {
+            @Override
+            public void onSensorChanged(SensorEvent event) {
+                float x = -event.values[2];
+                float y = -event.values[0];
+                PointF displaySize =
+                        mInputController.getFocusedDisplaySize().orElse(new PointF(0, 0));
+                if (Math.abs(x) > SENSOR_EVENT_THRESHOLD && Math.abs(y) > SENSOR_EVENT_THRESHOLD) {
+                    x *= SENSOR_EVENT_SCALE * displaySize.x;
+                    y *= SENSOR_EVENT_SCALE * displaySize.y;
+                    mInputController.sendMouseRelativeEvent(x, y);
+                }
+            }
+
+            @Override
+            public void onAccuracyChanged(Sensor sensor, int accuracy) {}
+        };
+
+        @Override
+        public void onResume() {
+            super.onResume();
+            mSensorManager = requireContext().getSystemService(SensorManager.class);
+            mSensorManager.registerListener(
+                    mSensorEventListener,
+                    mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE),
+                    SensorManager.SENSOR_DELAY_GAME);
+        }
+
+        @Override
+        public void onPause() {
+            super.onPause();
+            mSensorManager.unregisterListener(mSensorEventListener);
+        }
+
+        @Override
+        protected @StringRes int getViewTextResourceId() {
+            return R.string.remote_control_label;
+        }
+    }
+}
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java
index d7eb4ff..45f103f 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java
@@ -245,6 +245,10 @@
         return mVirtualDisplay.getDisplay().getDisplayId();
     }
 
+    PointF getDisplaySize() {
+        return new PointF(mWidth, mHeight);
+    }
+
     void onDisplayChanged() {
         if (mRotation != mVirtualDisplay.getDisplay().getRotation()) {
             mRotation = mVirtualDisplay.getDisplay().getRotation();
@@ -266,14 +270,7 @@
             return;
         }
         if (event.hasHomeEvent()) {
-            Intent homeIntent = new Intent(Intent.ACTION_MAIN);
-            homeIntent.addCategory(Intent.CATEGORY_HOME);
-            homeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            int targetDisplayId =
-                    mDisplayType == DISPLAY_TYPE_MIRROR ? Display.DEFAULT_DISPLAY : getDisplayId();
-            mContext.startActivity(
-                    homeIntent,
-                    ActivityOptions.makeBasic().setLaunchDisplayId(targetDisplayId).toBundle());
+            goHome();
         } else if (event.hasInputEvent()) {
             processInputEvent(event.getInputEvent());
         } else if (event.hasStopStreaming() && event.getStopStreaming().getPause()) {
@@ -284,6 +281,20 @@
         }
     }
 
+    void goHome() {
+        if (mDisplayType != DISPLAY_TYPE_HOME && mDisplayType != DISPLAY_TYPE_MIRROR) {
+            return;
+        }
+        Intent homeIntent = new Intent(Intent.ACTION_MAIN);
+        homeIntent.addCategory(Intent.CATEGORY_HOME);
+        homeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        int targetDisplayId =
+                mDisplayType == DISPLAY_TYPE_MIRROR ? Display.DEFAULT_DISPLAY : getDisplayId();
+        mContext.startActivity(
+                homeIntent,
+                ActivityOptions.makeBasic().setLaunchDisplayId(targetDisplayId).toBundle());
+    }
+
     private void processInputEvent(RemoteInputEvent inputEvent) {
         switch (inputEvent.getDeviceType()) {
             case DEVICE_TYPE_NONE:
@@ -350,17 +361,22 @@
 
     }
 
+    void processVirtualMouseEvent(Object mouseEvent) {
+        if (!createMouseIfNeeded()) {
+            return;
+        }
+        if (mouseEvent instanceof VirtualMouseButtonEvent) {
+            mMouse.sendButtonEvent((VirtualMouseButtonEvent) mouseEvent);
+        } else if (mouseEvent instanceof VirtualMouseScrollEvent) {
+            mMouse.sendScrollEvent((VirtualMouseScrollEvent) mouseEvent);
+        } else if (mouseEvent instanceof VirtualMouseRelativeEvent) {
+            mMouse.sendRelativeEvent((VirtualMouseRelativeEvent) mouseEvent);
+        }
+    }
+
     private void processMouseEvent(RemoteInputEvent inputEvent) {
-        if (mMouse == null) {
-            if (!VdmCompat.canCreateVirtualMouse(mContext)) {
-                return;
-            }
-            mMouse =
-                    mVirtualDevice.createVirtualMouse(
-                            new VirtualMouseConfig.Builder()
-                                    .setAssociatedDisplayId(getDisplayId())
-                                    .setInputDeviceName("vdmdemo-mouse" + mRemoteDisplayId)
-                                    .build());
+        if (!createMouseIfNeeded()) {
+            return;
         }
         if (inputEvent.hasMouseButtonEvent()) {
             mMouse.sendButtonEvent(
@@ -388,6 +404,18 @@
         }
     }
 
+    private boolean createMouseIfNeeded() {
+        if (mMouse == null && VdmCompat.canCreateVirtualMouse(mContext)) {
+            mMouse =
+                    mVirtualDevice.createVirtualMouse(
+                            new VirtualMouseConfig.Builder()
+                                    .setAssociatedDisplayId(getDisplayId())
+                                    .setInputDeviceName("vdmdemo-mouse" + mRemoteDisplayId)
+                                    .build());
+        }
+        return mMouse != null;
+    }
+
     private static int getVirtualTouchEventAction(int action) {
         return switch (action) {
             case MotionEvent.ACTION_POINTER_DOWN -> VirtualTouchEvent.ACTION_DOWN;