/
pointer_capture.go
361 lines (313 loc) · 13 KB
/
pointer_capture.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package arc
import (
"context"
"encoding/json"
"time"
"chromiumos/tast/common/android/ui"
"chromiumos/tast/errors"
"chromiumos/tast/local/arc"
"chromiumos/tast/local/bundles/cros/arc/motioninput"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/chrome/ash"
"chromiumos/tast/local/chrome/uiauto/mouse"
"chromiumos/tast/local/coords"
"chromiumos/tast/local/input"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: PointerCapture,
LacrosStatus: testing.LacrosVariantUnneeded,
Desc: "Checks that Pointer Capture works in Android",
Contacts: []string{"prabirmsp@chromium.org", "arc-framework+tast@google.com"},
Attr: []string{"group:mainline", "informational"},
SoftwareDeps: []string{"chrome", "android_vm"},
Fixture: "arcBooted",
})
}
// PointerCapture tests the Android Pointer Capture API support on ChromeOS. It uses a test
// application that requests Pointer Capture and verifies the relative movements the app receives
// when injecting events into ChromeOS through a uinput mouse.
// More about Pointer Capture: https://developer.android.com/training/gestures/movement#pointer-capture
func PointerCapture(ctx context.Context, s *testing.State) {
p := s.FixtValue().(*arc.PreData)
cr := p.Chrome
a := p.ARC
d := p.UIDevice
tconn, err := cr.TestAPIConn(ctx)
if err != nil {
s.Fatal("Failed to create test API connection: ", err)
}
s.Log("Installing apk ", motioninput.APK)
if err := a.Install(ctx, arc.APKPath(motioninput.APK)); err != nil {
s.Fatalf("Failed installing %s: %v", motioninput.APK, err)
}
runSubtest := func(ctx context.Context, s *testing.State, subtestFunc pointerCaptureSubtestFunc) {
test := pointerCaptureSubtestState{}
test.arc = a
test.tconn = tconn
test.d = d
act, err := arc.NewActivity(a, motioninput.Package, motioninput.AutoPointerCaptureActivity)
if err != nil {
s.Fatal("Failed to create an activity: ", err)
}
defer act.Close()
if err := act.StartWithDefaultOptions(ctx, tconn); err != nil {
s.Fatal("Failed to start an activity: ", err)
}
defer act.Stop(ctx, tconn)
if err := ash.WaitForVisible(ctx, tconn, motioninput.Package); err != nil {
s.Fatal("Failed to wait for activity to be visible: ", err)
}
test.mew, err = input.Mouse(ctx)
if err != nil {
s.Fatal("Failed to create mouse device: ", err)
}
defer test.mew.Close()
s.Log("Enabling pointer capture")
if err := enablePointerCapture(ctx, tconn); err != nil {
s.Fatal("Failed to enable pointer capture: ", err)
}
if err := expectPointerCaptureState(ctx, d, true); err != nil {
s.Fatal("Failed to verify that pointer capture is enabled: ", err)
}
test.tester = motioninput.NewTester(tconn, d, act)
if err := test.tester.ClearMotionEvents(ctx); err != nil {
s.Fatal("Failed to clear events: ", err)
}
subtestFunc(ctx, s, test)
}
for _, subtest := range []struct {
Name string
Func pointerCaptureSubtestFunc
}{
{
Name: "Pointer Capture sends relative movements",
Func: verifyPointerCaptureRelativeMovement,
}, {
Name: "Pointer Capture is not restricted by display bounds",
Func: verifyPointerCaptureBounds,
}, {
Name: "Pointer Capture buttons",
Func: verifyPointerCaptureButtons,
}, {
Name: "Pointer Capture is disabled when Chrome is focused",
Func: verifyPointerCaptureDisabledWhenChromeFocused,
}, {
Name: "Pointer Capture is re-enabled after switching focus with the keyboard",
Func: verifyPointerCaptureWithKeyboardFocusChange,
},
} {
s.Run(ctx, subtest.Name, func(ctx context.Context, s *testing.State) {
runSubtest(ctx, s, subtest.Func)
})
}
}
type pointerCaptureState struct {
Enabled bool `json:"pointer_capture_enabled"`
}
// enablePointerCapture clicks at the center of the test application, which will make the test app
// trigger Pointer Capture.
func enablePointerCapture(ctx context.Context, tconn *chrome.TestConn) error {
// Click on the capture_view using the ui mouse. This ensures that the Ash window is in focus.
// We cannot use UI Automator to click on the capture_view because that does not guarantee the
// window is in focus in Ash as there could be something like a pop-up notification that
// actually has focus.
w, err := ash.GetARCAppWindowInfo(ctx, tconn, motioninput.Package)
if err != nil {
return errors.Wrap(err, "failed to get ARC app window info")
}
center := w.BoundsInRoot.CenterPoint()
if err := mouse.Click(tconn, center, mouse.LeftButton)(ctx); err != nil {
return errors.Wrap(err, "failed to click in the app window to enable pointer capture")
}
return nil
}
// readPointerCaptureState unmarshalls the JSON string in the TextView representing the
// Pointer Capture state in the test application.
func readPointerCaptureState(ctx context.Context, d *ui.Device) (*pointerCaptureState, error) {
view := d.Object(ui.ID(motioninput.Package + ":id/pointer_capture_state"))
if err := view.WaitForExists(ctx, 5*time.Second); err != nil {
return nil, err
}
text, err := view.GetText(ctx)
if err != nil {
return nil, err
}
var state pointerCaptureState
if err := json.Unmarshal([]byte(text), &state); err != nil {
return nil, err
}
return &state, nil
}
// expectPointerCaptureState polls readPointerCaptureState repeatedly until Pointer Capture is
// equal to the expected value.
func expectPointerCaptureState(ctx context.Context, d *ui.Device, enabled bool) error {
return testing.Poll(ctx, func(ctx context.Context) error {
state, err := readPointerCaptureState(ctx, d)
if err != nil {
return err
}
if state.Enabled != enabled {
return errors.Errorf("unexpected Pointer Capture state: want: %t, got: %t", enabled, state.Enabled)
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second})
}
// pointerCaptureSubtestState holds values that are initialized to be used by the subtests.
type pointerCaptureSubtestState struct {
tester *motioninput.Tester
mew *input.MouseEventWriter
arc *arc.ARC
tconn *chrome.TestConn
d *ui.Device
}
// pointerCaptureSubtestFunc represents a subtest function.
type pointerCaptureSubtestFunc func(ctx context.Context, s *testing.State, t pointerCaptureSubtestState)
// ensureRelativeMovement is a helper function that injects a relative mouse movements through a uinput
// mouse device and checks that a relative movement was sent to the application.
func ensureRelativeMovement(ctx context.Context, t pointerCaptureSubtestState, delta coords.Point) error {
if err := t.mew.Move(int32(delta.X), int32(delta.Y)); err != nil {
return errors.Wrapf(err, "failed to move mouse by (%d, %d)", delta.X, delta.Y)
}
// We only verify the action and source of each event and not the magnitude of the movements
// because ChromeOS applies mouse acceleration which changes the magnitude.
matcher := motioninput.ActionSourceMatcher(motioninput.ActionMove, motioninput.SourceMouseRelative)
if err := t.tester.ExpectEventsAndClear(ctx, matcher); err != nil {
return errors.Wrap(err, "failed to verify motion event and clear")
}
if err := t.tester.ClearMotionEvents(ctx); err != nil {
return errors.Wrap(err, "failed to clear events")
}
return nil
}
// verifyPointerCaptureRelativeMovement is a subtest that verifies that mouse movements injected when Pointer
// Capture is enabled are sent to the app as relative movements.
func verifyPointerCaptureRelativeMovement(ctx context.Context, s *testing.State, t pointerCaptureSubtestState) {
if err := ensureRelativeMovement(ctx, t, coords.NewPoint(10, 10)); err != nil {
s.Fatal("Failed to verify relative movement: ", err)
}
}
// verifyPointerCaptureBounds is a subtest that verifies mouse movement is not restricted by the
// bounds of the display, since only relative movements are reported when Pointer Capture is
// enabled. This is tested by injecting a large number of relative mouse movements in a single
// direction.
func verifyPointerCaptureBounds(ctx context.Context, s *testing.State, t pointerCaptureSubtestState) {
delta := coords.NewPoint(-100, -100)
for i := 0; i < 20; i++ {
if err := ensureRelativeMovement(ctx, t, delta); err != nil {
s.Fatal("Failed to verify relative movement: ", err)
}
}
}
// verifyPointerCaptureButtons is a subtest that ensures mouse button functionality when Pointer
// Capture is enabled.
func verifyPointerCaptureButtons(ctx context.Context, s *testing.State, t pointerCaptureSubtestState) {
if err := t.mew.Click(); err != nil {
s.Fatal("Failed to click mouse button: ", err)
}
matcher := func(a motioninput.Action, pressure float64) motioninput.Matcher {
return motioninput.SinglePointerMatcher(a, motioninput.SourceMouseRelative, coords.NewPoint(0, 0), pressure)
}
hoverEnterMatcher := motioninput.ActionSourceMatcher(motioninput.ActionHoverEnter, motioninput.SourceMouseRelative)
if err := t.tester.ExpectEventsAndClear(ctx,
matcher(motioninput.ActionDown, 1),
matcher(motioninput.ActionButtonPress, 1),
matcher(motioninput.ActionButtonRelease, 0),
matcher(motioninput.ActionUp, 0),
hoverEnterMatcher); err != nil {
s.Fatal("Failed to clear motion events and clear: ", err)
}
}
// verifyPointerCaptureDisabledWhenChromeFocused is a subtest that ensures Pointer Capture is disabled when
// a Chrome window comes into focus.
func verifyPointerCaptureDisabledWhenChromeFocused(ctx context.Context, s *testing.State, t pointerCaptureSubtestState) {
kb, err := input.Keyboard(ctx)
if err != nil {
s.Fatal("Failed to find keyboard: ", err)
}
defer kb.Close()
if err := t.tester.WaitForTestAppFocused(ctx, true); err != nil {
s.Fatal("Failed to ensure the test app was initially focused: ", err)
}
// Press the search key to bring the launcher into focus.
if err := kb.Accel(ctx, "Search"); err != nil {
s.Fatal("Failed to press Search: ", err)
}
if err := t.tester.WaitForTestAppFocused(ctx, false); err != nil {
s.Fatal("Failed to ensure the test app lost focus: ", err)
}
// Press the search key again to hide the launcher.
if err := kb.Accel(ctx, "Search"); err != nil {
s.Fatal("Failed to press Search: ", err)
}
if err := t.tester.WaitForTestAppFocused(ctx, true); err != nil {
s.Fatal("Failed to ensure the test app regained focused: ", err)
}
// Pointer Capture should be enabled when window gains focus.
if err := expectPointerCaptureState(ctx, t.d, true); err != nil {
s.Fatal("Failed to verify that pointer capture is enabled: ", err)
}
// The first move event is consumed by Chrome (b/185837950), so send an extra one.
if err := t.mew.Move(10, 10); err != nil {
s.Fatal("Failed to move mouse: ", err)
}
// Clear events, since hover events could have been generated before Pointer Capture was re-enabled.
if err := t.tester.ClearMotionEvents(ctx); err != nil {
s.Fatal("Failed to clear events: ", err)
}
if err := ensureRelativeMovement(ctx, t, coords.NewPoint(10, 10)); err != nil {
s.Fatal("Failed to verify relative movement: ", err)
}
}
// verifyPointerCaptureWithKeyboardFocusChange is a subtest that ensures Pointer Capture is disabled when
// the activity loses focus and re-gains Pointer Capture when it is focused again using the keyboard.
func verifyPointerCaptureWithKeyboardFocusChange(ctx context.Context, s *testing.State, t pointerCaptureSubtestState) {
// Launch the settings activity to make the Pointer Capture Activity lose focus.
const (
settingsPackage = "com.android.settings"
settingsActivity = ".Settings"
)
act, err := arc.NewActivity(t.arc, settingsPackage, settingsActivity)
if err != nil {
s.Fatal("Failed to create an activity: ", err)
}
defer act.Close()
if err := act.StartWithDefaultOptions(ctx, t.tconn); err != nil {
s.Fatal("Failed to start an activity: ", err)
}
defer act.Stop(ctx, t.tconn)
if err := ash.WaitForVisible(ctx, t.tconn, settingsPackage); err != nil {
s.Fatal("Failed to wait for activity to be visible: ", err)
}
// Pointer Capture should be disabled when window loses focus.
if err := expectPointerCaptureState(ctx, t.d, false); err != nil {
s.Fatal("Failed to verify that pointer capture is disabled: ", err)
}
kb, err := input.Keyboard(ctx)
if err != nil {
s.Fatal("Failed to find keyboard: ", err)
}
defer kb.Close()
if err := kb.Accel(ctx, "Alt+Tab"); err != nil {
s.Fatal("Failed to press Alt+Tab to switch windows: ", err)
}
// The activity will automatically request pointer capture when it gains focus.
if err := expectPointerCaptureState(ctx, t.d, true); err != nil {
s.Fatal("Failed to verify that pointer capture is enabled: ", err)
}
// The first move event is consumed by Chrome (b/185837950), so send an extra one.
if err := t.mew.Move(10, 10); err != nil {
s.Fatal("Failed to move mouse: ", err)
}
// Clear events, since hover events could have been generated before Pointer Capture was re-enabled.
if err := t.tester.ClearMotionEvents(ctx); err != nil {
s.Fatal("Failed to clear events: ", err)
}
if err := ensureRelativeMovement(ctx, t, coords.NewPoint(10, 10)); err != nil {
s.Fatal("Failed to verify relative movement: ", err)
}
}