1 /*
2  * Copyright 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.android.server.input;
18 
19 import static android.view.InputDevice.SOURCE_MOUSE;
20 import static android.view.InputDevice.SOURCE_ROTARY_ENCODER;
21 import static android.view.MotionEvent.ACTION_SCROLL;
22 import static android.view.MotionEvent.AXIS_HSCROLL;
23 import static android.view.MotionEvent.AXIS_SCROLL;
24 import static android.view.MotionEvent.AXIS_VSCROLL;
25 import static android.view.MotionEvent.AXIS_X;
26 import static android.view.MotionEvent.AXIS_Y;
27 
28 import static com.google.common.truth.Truth.assertThat;
29 import static com.google.common.truth.Truth.assertWithMessage;
30 
31 import android.os.Binder;
32 import android.view.InputEvent;
33 import android.view.MotionEvent;
34 
35 import androidx.test.runner.AndroidJUnit4;
36 
37 import org.junit.Before;
38 import org.junit.Test;
39 import org.junit.runner.RunWith;
40 
41 import java.io.FileDescriptor;
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.function.BiConsumer;
46 
47 /**
48  * Build/Install/Run:
49  * atest InputShellCommandTest
50  */
51 @RunWith(AndroidJUnit4.class)
52 public class InputShellCommandTest {
53     private TestInputEventInjector mInputEventInjector = new TestInputEventInjector();
54 
55     private InputShellCommand mCommand;
56 
57     @Before
setUp()58     public void setUp() throws Exception {
59         mCommand = new InputShellCommand(mInputEventInjector);
60     }
61 
62     @Test
testScroll_withPointerSource_noAxisOption()63     public void testScroll_withPointerSource_noAxisOption() {
64         runCommand("mouse scroll 2 -3");
65 
66         MotionEvent event = (MotionEvent) getSingleInjectedInputEvent();
67 
68         assertSourceAndAction(event, SOURCE_MOUSE, ACTION_SCROLL);
69         assertAxisValues(event, Map.of(AXIS_X, 2f, AXIS_Y, -3f));
70     }
71 
72     @Test
testScroll_withPointerSource_withScrollAxisOptions()73     public void testScroll_withPointerSource_withScrollAxisOptions() {
74         runCommand("mouse scroll 1 -2 --axis HSCROLL,3 --axis VSCROLL,1.7 --axis SCROLL,-4");
75 
76         MotionEvent event = (MotionEvent) getSingleInjectedInputEvent();
77 
78         assertSourceAndAction(event, SOURCE_MOUSE, ACTION_SCROLL);
79         assertAxisValues(
80                 event,
81                 Map.of(
82                         AXIS_X, 1f,
83                         AXIS_Y, -2f,
84                         AXIS_HSCROLL, 3f,
85                         AXIS_VSCROLL, 1.7f,
86                         AXIS_SCROLL, -4f));
87     }
88 
89     @Test
testScroll_withNonPointerSource_noAxisOption()90     public void testScroll_withNonPointerSource_noAxisOption() {
91         runCommand("rotaryencoder scroll");
92 
93         MotionEvent event = (MotionEvent) getSingleInjectedInputEvent();
94 
95         assertSourceAndAction(event, SOURCE_ROTARY_ENCODER, ACTION_SCROLL);
96     }
97 
98     @Test
testScroll_withNonPointerSource_withScrollAxisOptions()99     public void testScroll_withNonPointerSource_withScrollAxisOptions() {
100         runCommand("rotaryencoder scroll --axis HSCROLL,3 --axis VSCROLL,1.7 --axis SCROLL,-4");
101 
102         MotionEvent event = (MotionEvent) getSingleInjectedInputEvent();
103 
104         assertSourceAndAction(event, SOURCE_ROTARY_ENCODER, ACTION_SCROLL);
105         assertAxisValues(event, Map.of(AXIS_HSCROLL, 3f, AXIS_VSCROLL, 1.7f, AXIS_SCROLL, -4f));
106     }
107 
108     @Test
testDefaultScrollSource()109     public void testDefaultScrollSource() {
110         runCommand("scroll --axis SCROLL,-4");
111 
112         MotionEvent event = (MotionEvent) getSingleInjectedInputEvent();
113 
114         assertSourceAndAction(event, SOURCE_ROTARY_ENCODER, ACTION_SCROLL);
115         assertAxisValues(event, Map.of(AXIS_SCROLL, -4f));
116     }
117 
118     @Test
testInvalidScrollCommands()119     public void testInvalidScrollCommands() {
120         runCommand("scroll --sdaxis SCROLL,-4"); // invalid option
121         runCommand("scroll --axis MYAXIS,-4"); // invalid axis
122         runCommand("scroll --AXIS SCROLL,-4"); // invalid axis option key
123         runCommand("scroll --axis SCROLL,-4abc"); // invalid axis value
124 
125         assertThat(mInputEventInjector.mInjectedEvents).isEmpty();
126     }
127 
128     @Test
testInvalidKeyEventCommandArgsCombination()129     public void testInvalidKeyEventCommandArgsCombination() {
130         // --duration and --longpress must not be sent together
131         runCommand("keyevent --duration 1000 --longpress KEYCODE_A");
132 
133         assertThat(mInputEventInjector.mInjectedEvents).isEmpty();
134     }
135 
getSingleInjectedInputEvent()136     private InputEvent getSingleInjectedInputEvent() {
137         assertThat(mInputEventInjector.mInjectedEvents).hasSize(1);
138         return mInputEventInjector.mInjectedEvents.get(0);
139     }
140 
assertSourceAndAction(MotionEvent event, int source, int action)141     private void assertSourceAndAction(MotionEvent event, int source, int action) {
142         assertThat(event.getSource()).isEqualTo(source);
143         assertThat(event.getAction()).isEqualTo(action);
144     }
145 
assertAxisValues(MotionEvent event, Map<Integer, Float> expectedValues)146     private void assertAxisValues(MotionEvent event, Map<Integer, Float> expectedValues) {
147         for (var entry : expectedValues.entrySet()) {
148             final int axis = entry.getKey();
149             final float expectedValue = entry.getValue();
150             final float axisValue = event.getAxisValue(axis);
151             assertWithMessage(
152                     String.format(
153                             "Expected [%f], found [%f] for axis %s",
154                             expectedValue,
155                             axisValue,
156                             MotionEvent.axisToString(axis)))
157                     .that(axisValue).isEqualTo(expectedValue);
158         }
159     }
160 
runCommand(String cmd)161     private void runCommand(String cmd) {
162         mCommand.exec(
163                 new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(),
164                 cmd.split(" ") /* args */);
165     }
166 
167     private static class TestInputEventInjector implements BiConsumer<InputEvent, Integer> {
168         List<InputEvent> mInjectedEvents = new ArrayList<>();
169 
170         @Override
accept(InputEvent event, Integer injectMode)171         public void accept(InputEvent event, Integer injectMode) {
172             mInjectedEvents.add(event);
173         }
174     }
175 }
176