blob: b139e8766fb394f0d0ce79dd1a22f4d629701c4e [file] [log] [blame]
Paul Ramirez5d59a422024-10-01 15:59:16 +00001/*
2 * Copyright (C) 2024 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#include <input/InputConsumerNoResampling.h>
18
19#include <chrono>
20#include <memory>
21#include <string>
22#include <vector>
23
24#include <TestEventMatchers.h>
25#include <TestInputChannel.h>
26#include <attestation/HmacKeyManager.h>
27#include <gmock/gmock.h>
28#include <gtest/gtest.h>
29#include <input/BlockingQueue.h>
30#include <input/InputEventBuilders.h>
31#include <input/Resampler.h>
32#include <utils/Looper.h>
33#include <utils/StrongPointer.h>
34
35namespace android {
36namespace {
37
38using std::chrono::nanoseconds;
39using namespace std::chrono_literals;
40
41struct Pointer {
42 int32_t id{0};
43 float x{0.0f};
44 float y{0.0f};
45 ToolType toolType{ToolType::FINGER};
46 bool isResampled{false};
47
48 PointerBuilder asPointerBuilder() const {
49 return PointerBuilder{id, toolType}.x(x).y(y).isResampled(isResampled);
50 }
51};
52
53struct InputEventEntry {
54 std::chrono::nanoseconds eventTime{0};
55 std::vector<Pointer> pointers{};
56 int32_t action{-1};
57};
58
59} // namespace
60
61class InputConsumerResamplingTest : public ::testing::Test, public InputConsumerCallbacks {
62protected:
63 InputConsumerResamplingTest()
64 : mClientTestChannel{std::make_shared<TestInputChannel>("TestChannel")},
65 mLooper{sp<Looper>::make(/*allowNonCallbacks=*/false)} {
66 Looper::setForThread(mLooper);
67 mConsumer = std::make_unique<
68 InputConsumerNoResampling>(mClientTestChannel, mLooper, *this,
69 []() { return std::make_unique<LegacyResampler>(); });
70 }
71
72 void invokeLooperCallback() const {
73 sp<LooperCallback> callback;
74 ASSERT_TRUE(mLooper->getFdStateDebug(mClientTestChannel->getFd(), /*ident=*/nullptr,
75 /*events=*/nullptr, &callback, /*data=*/nullptr));
76 ASSERT_NE(callback, nullptr);
77 callback->handleEvent(mClientTestChannel->getFd(), ALOOPER_EVENT_INPUT, /*data=*/nullptr);
78 }
79
80 InputMessage nextPointerMessage(const InputEventEntry& entry);
81
82 void assertReceivedMotionEvent(const std::vector<InputEventEntry>& expectedEntries);
83
84 std::shared_ptr<TestInputChannel> mClientTestChannel;
85 sp<Looper> mLooper;
86 std::unique_ptr<InputConsumerNoResampling> mConsumer;
87
88 BlockingQueue<std::unique_ptr<KeyEvent>> mKeyEvents;
89 BlockingQueue<std::unique_ptr<MotionEvent>> mMotionEvents;
90 BlockingQueue<std::unique_ptr<FocusEvent>> mFocusEvents;
91 BlockingQueue<std::unique_ptr<CaptureEvent>> mCaptureEvents;
92 BlockingQueue<std::unique_ptr<DragEvent>> mDragEvents;
93 BlockingQueue<std::unique_ptr<TouchModeEvent>> mTouchModeEvents;
94
95private:
96 uint32_t mLastSeq{0};
97 size_t mOnBatchedInputEventPendingInvocationCount{0};
98
99 // InputConsumerCallbacks interface
100 void onKeyEvent(std::unique_ptr<KeyEvent> event, uint32_t seq) override {
101 mKeyEvents.push(std::move(event));
102 mConsumer->finishInputEvent(seq, true);
103 }
104 void onMotionEvent(std::unique_ptr<MotionEvent> event, uint32_t seq) override {
105 mMotionEvents.push(std::move(event));
106 mConsumer->finishInputEvent(seq, true);
107 }
108 void onBatchedInputEventPending(int32_t pendingBatchSource) override {
109 if (!mConsumer->probablyHasInput()) {
110 ADD_FAILURE() << "should deterministically have input because there is a batch";
111 }
112 ++mOnBatchedInputEventPendingInvocationCount;
113 }
114 void onFocusEvent(std::unique_ptr<FocusEvent> event, uint32_t seq) override {
115 mFocusEvents.push(std::move(event));
116 mConsumer->finishInputEvent(seq, true);
117 }
118 void onCaptureEvent(std::unique_ptr<CaptureEvent> event, uint32_t seq) override {
119 mCaptureEvents.push(std::move(event));
120 mConsumer->finishInputEvent(seq, true);
121 }
122 void onDragEvent(std::unique_ptr<DragEvent> event, uint32_t seq) override {
123 mDragEvents.push(std::move(event));
124 mConsumer->finishInputEvent(seq, true);
125 }
126 void onTouchModeEvent(std::unique_ptr<TouchModeEvent> event, uint32_t seq) override {
127 mTouchModeEvents.push(std::move(event));
128 mConsumer->finishInputEvent(seq, true);
129 }
130};
131
132InputMessage InputConsumerResamplingTest::nextPointerMessage(const InputEventEntry& entry) {
133 ++mLastSeq;
134 InputMessageBuilder messageBuilder = InputMessageBuilder{InputMessage::Type::MOTION, mLastSeq}
135 .eventTime(entry.eventTime.count())
136 .deviceId(1)
137 .action(entry.action)
138 .downTime(0);
139 for (const Pointer& pointer : entry.pointers) {
140 messageBuilder.pointer(pointer.asPointerBuilder());
141 }
142 return messageBuilder.build();
143}
144
145void InputConsumerResamplingTest::assertReceivedMotionEvent(
146 const std::vector<InputEventEntry>& expectedEntries) {
147 std::unique_ptr<MotionEvent> motionEvent = mMotionEvents.pop();
148 ASSERT_NE(motionEvent, nullptr);
149
150 ASSERT_EQ(motionEvent->getHistorySize() + 1, expectedEntries.size());
151
152 for (size_t sampleIndex = 0; sampleIndex < expectedEntries.size(); ++sampleIndex) {
153 SCOPED_TRACE("sampleIndex: " + std::to_string(sampleIndex));
154 const InputEventEntry& expectedEntry = expectedEntries[sampleIndex];
155 EXPECT_EQ(motionEvent->getHistoricalEventTime(sampleIndex),
156 expectedEntry.eventTime.count());
157 EXPECT_EQ(motionEvent->getPointerCount(), expectedEntry.pointers.size());
158 EXPECT_EQ(motionEvent->getAction(), expectedEntry.action);
159
160 for (size_t pointerIndex = 0; pointerIndex < expectedEntry.pointers.size();
161 ++pointerIndex) {
162 SCOPED_TRACE("pointerIndex: " + std::to_string(pointerIndex));
163 ssize_t eventPointerIndex =
164 motionEvent->findPointerIndex(expectedEntry.pointers[pointerIndex].id);
165 EXPECT_EQ(motionEvent->getHistoricalRawX(eventPointerIndex, sampleIndex),
166 expectedEntry.pointers[pointerIndex].x);
167 EXPECT_EQ(motionEvent->getHistoricalRawY(eventPointerIndex, sampleIndex),
168 expectedEntry.pointers[pointerIndex].y);
169 EXPECT_EQ(motionEvent->getHistoricalX(eventPointerIndex, sampleIndex),
170 expectedEntry.pointers[pointerIndex].x);
171 EXPECT_EQ(motionEvent->getHistoricalY(eventPointerIndex, sampleIndex),
172 expectedEntry.pointers[pointerIndex].y);
173 EXPECT_EQ(motionEvent->isResampled(pointerIndex, sampleIndex),
174 expectedEntry.pointers[pointerIndex].isResampled);
175 }
176 }
177}
178
179/**
180 * Timeline
181 * ---------+------------------+------------------+--------+-----------------+----------------------
182 * 0 ms 10 ms 20 ms 25 ms 35 ms
183 * ACTION_DOWN ACTION_MOVE ACTION_MOVE ^ ^
184 * | |
185 * resampled value |
186 * frameTime
187 * Typically, the prediction is made for time frameTime - RESAMPLE_LATENCY, or 30 ms in this case,
188 * where RESAMPLE_LATENCY equals 5 milliseconds. However, that would be 10 ms later than the last
189 * real sample (which came in at 20 ms). Therefore, the resampling should happen at 20 ms +
190 * RESAMPLE_MAX_PREDICTION = 28 ms, where RESAMPLE_MAX_PREDICTION equals 8 milliseconds. In this
191 * situation, though, resample time is further limited by taking half of the difference between the
192 * last two real events, which would put this time at: 20 ms + (20 ms - 10 ms) / 2 = 25 ms.
193 */
194TEST_F(InputConsumerResamplingTest, EventIsResampled) {
Paul Ramirez7f1efed2024-09-29 23:55:23 +0000195 // Send the initial ACTION_DOWN separately, so that the first consumed event will only return an
Paul Ramirez5d59a422024-10-01 15:59:16 +0000196 // InputEvent with a single action.
197 mClientTestChannel->enqueueMessage(nextPointerMessage(
198 {0ms, {Pointer{.id = 0, .x = 10.0f, .y = 20.0f}}, AMOTION_EVENT_ACTION_DOWN}));
199
200 mClientTestChannel->assertNoSentMessages();
201
202 invokeLooperCallback();
203 assertReceivedMotionEvent({InputEventEntry{0ms,
204 {Pointer{.id = 0, .x = 10.0f, .y = 20.0f}},
205 AMOTION_EVENT_ACTION_DOWN}});
206
207 // Two ACTION_MOVE events 10 ms apart that move in X direction and stay still in Y
208 mClientTestChannel->enqueueMessage(nextPointerMessage(
209 {10ms, {Pointer{.id = 0, .x = 20.0f, .y = 30.0f}}, AMOTION_EVENT_ACTION_MOVE}));
210 mClientTestChannel->enqueueMessage(nextPointerMessage(
211 {20ms, {Pointer{.id = 0, .x = 30.0f, .y = 30.0f}}, AMOTION_EVENT_ACTION_MOVE}));
212
213 invokeLooperCallback();
214 mConsumer->consumeBatchedInputEvents(nanoseconds{35ms}.count());
215 assertReceivedMotionEvent(
216 {InputEventEntry{10ms,
217 {Pointer{.id = 0, .x = 20.0f, .y = 30.0f}},
218 AMOTION_EVENT_ACTION_MOVE},
219 InputEventEntry{20ms,
220 {Pointer{.id = 0, .x = 30.0f, .y = 30.0f}},
221 AMOTION_EVENT_ACTION_MOVE},
222 InputEventEntry{25ms,
223 {Pointer{.id = 0, .x = 35.0f, .y = 30.0f, .isResampled = true}},
224 AMOTION_EVENT_ACTION_MOVE}});
225
226 mClientTestChannel->assertFinishMessage(/*seq=*/1, /*handled=*/true);
227 mClientTestChannel->assertFinishMessage(/*seq=*/2, /*handled=*/true);
228 mClientTestChannel->assertFinishMessage(/*seq=*/3, /*handled=*/true);
229}
230
231/**
232 * Same as above test, but use pointer id=1 instead of 0 to make sure that system does not
233 * have these hardcoded.
234 */
235TEST_F(InputConsumerResamplingTest, EventIsResampledWithDifferentId) {
Paul Ramirez7f1efed2024-09-29 23:55:23 +0000236 // Send the initial ACTION_DOWN separately, so that the first consumed event will only return an
Paul Ramirez5d59a422024-10-01 15:59:16 +0000237 // InputEvent with a single action.
238 mClientTestChannel->enqueueMessage(nextPointerMessage(
239 {0ms, {Pointer{.id = 1, .x = 10.0f, .y = 20.0f}}, AMOTION_EVENT_ACTION_DOWN}));
240
241 mClientTestChannel->assertNoSentMessages();
242
243 invokeLooperCallback();
244 assertReceivedMotionEvent({InputEventEntry{0ms,
245 {Pointer{.id = 1, .x = 10.0f, .y = 20.0f}},
246 AMOTION_EVENT_ACTION_DOWN}});
247
248 // Two ACTION_MOVE events 10 ms apart that move in X direction and stay still in Y
249 mClientTestChannel->enqueueMessage(nextPointerMessage(
250 {10ms, {Pointer{.id = 1, .x = 20.0f, .y = 30.0f}}, AMOTION_EVENT_ACTION_MOVE}));
251 mClientTestChannel->enqueueMessage(nextPointerMessage(
252 {20ms, {Pointer{.id = 1, .x = 30.0f, .y = 30.0f}}, AMOTION_EVENT_ACTION_MOVE}));
253
254 invokeLooperCallback();
255 mConsumer->consumeBatchedInputEvents(nanoseconds{35ms}.count());
256 assertReceivedMotionEvent(
257 {InputEventEntry{10ms,
258 {Pointer{.id = 1, .x = 20.0f, .y = 30.0f}},
259 AMOTION_EVENT_ACTION_MOVE},
260 InputEventEntry{20ms,
261 {Pointer{.id = 1, .x = 30.0f, .y = 30.0f}},
262 AMOTION_EVENT_ACTION_MOVE},
263 InputEventEntry{25ms,
264 {Pointer{.id = 1, .x = 35.0f, .y = 30.0f, .isResampled = true}},
265 AMOTION_EVENT_ACTION_MOVE}});
266
267 mClientTestChannel->assertFinishMessage(/*seq=*/1, /*handled=*/true);
268 mClientTestChannel->assertFinishMessage(/*seq=*/2, /*handled=*/true);
269 mClientTestChannel->assertFinishMessage(/*seq=*/3, /*handled=*/true);
270}
271
272/**
273 * Stylus pointer coordinates are resampled.
274 */
275TEST_F(InputConsumerResamplingTest, StylusEventIsResampled) {
Paul Ramirez7f1efed2024-09-29 23:55:23 +0000276 // Send the initial ACTION_DOWN separately, so that the first consumed event will only return an
Paul Ramirez5d59a422024-10-01 15:59:16 +0000277 // InputEvent with a single action.
278 mClientTestChannel->enqueueMessage(nextPointerMessage(
279 {0ms,
280 {Pointer{.id = 0, .x = 10.0f, .y = 20.0f, .toolType = ToolType::STYLUS}},
281 AMOTION_EVENT_ACTION_DOWN}));
282
283 mClientTestChannel->assertNoSentMessages();
284
285 invokeLooperCallback();
286 assertReceivedMotionEvent({InputEventEntry{0ms,
287 {Pointer{.id = 0,
288 .x = 10.0f,
289 .y = 20.0f,
290 .toolType = ToolType::STYLUS}},
291 AMOTION_EVENT_ACTION_DOWN}});
292
293 // Two ACTION_MOVE events 10 ms apart that move in X direction and stay still in Y
294 mClientTestChannel->enqueueMessage(nextPointerMessage(
295 {10ms,
296 {Pointer{.id = 0, .x = 20.0f, .y = 30.0f, .toolType = ToolType::STYLUS}},
297 AMOTION_EVENT_ACTION_MOVE}));
298 mClientTestChannel->enqueueMessage(nextPointerMessage(
299 {20ms,
300 {Pointer{.id = 0, .x = 30.0f, .y = 30.0f, .toolType = ToolType::STYLUS}},
301 AMOTION_EVENT_ACTION_MOVE}));
302
303 invokeLooperCallback();
304 mConsumer->consumeBatchedInputEvents(nanoseconds{35ms}.count());
305 assertReceivedMotionEvent({InputEventEntry{10ms,
306 {Pointer{.id = 0,
307 .x = 20.0f,
308 .y = 30.0f,
309 .toolType = ToolType::STYLUS}},
310 AMOTION_EVENT_ACTION_MOVE},
311 InputEventEntry{20ms,
312 {Pointer{.id = 0,
313 .x = 30.0f,
314 .y = 30.0f,
315 .toolType = ToolType::STYLUS}},
316 AMOTION_EVENT_ACTION_MOVE},
317 InputEventEntry{25ms,
318 {Pointer{.id = 0,
319 .x = 35.0f,
320 .y = 30.0f,
321 .toolType = ToolType::STYLUS,
322 .isResampled = true}},
323 AMOTION_EVENT_ACTION_MOVE}});
324
325 mClientTestChannel->assertFinishMessage(/*seq=*/1, /*handled=*/true);
326 mClientTestChannel->assertFinishMessage(/*seq=*/2, /*handled=*/true);
327 mClientTestChannel->assertFinishMessage(/*seq=*/3, /*handled=*/true);
328}
329
330/**
331 * Mouse pointer coordinates are resampled.
332 */
333TEST_F(InputConsumerResamplingTest, MouseEventIsResampled) {
Paul Ramirez7f1efed2024-09-29 23:55:23 +0000334 // Send the initial ACTION_DOWN separately, so that the first consumed event will only return an
Paul Ramirez5d59a422024-10-01 15:59:16 +0000335 // InputEvent with a single action.
Paul Ramirez5d59a422024-10-01 15:59:16 +0000336 mClientTestChannel->enqueueMessage(nextPointerMessage(
337 {0ms,
338 {Pointer{.id = 0, .x = 10.0f, .y = 20.0f, .toolType = ToolType::MOUSE}},
339 AMOTION_EVENT_ACTION_DOWN}));
340
341 mClientTestChannel->assertNoSentMessages();
342
343 invokeLooperCallback();
344 assertReceivedMotionEvent({InputEventEntry{0ms,
345 {Pointer{.id = 0,
346 .x = 10.0f,
347 .y = 20.0f,
348 .toolType = ToolType::MOUSE}},
349 AMOTION_EVENT_ACTION_DOWN}});
350
351 // Two ACTION_MOVE events 10 ms apart that move in X direction and stay still in Y
352 mClientTestChannel->enqueueMessage(nextPointerMessage(
353 {10ms,
354 {Pointer{.id = 0, .x = 20.0f, .y = 30.0f, .toolType = ToolType::MOUSE}},
355 AMOTION_EVENT_ACTION_MOVE}));
356 mClientTestChannel->enqueueMessage(nextPointerMessage(
357 {20ms,
358 {Pointer{.id = 0, .x = 30.0f, .y = 30.0f, .toolType = ToolType::MOUSE}},
359 AMOTION_EVENT_ACTION_MOVE}));
360
361 invokeLooperCallback();
362 mConsumer->consumeBatchedInputEvents(nanoseconds{35ms}.count());
363 assertReceivedMotionEvent({InputEventEntry{10ms,
364 {Pointer{.id = 0,
365 .x = 20.0f,
366 .y = 30.0f,
367 .toolType = ToolType::MOUSE}},
368 AMOTION_EVENT_ACTION_MOVE},
369 InputEventEntry{20ms,
370 {Pointer{.id = 0,
371 .x = 30.0f,
372 .y = 30.0f,
373 .toolType = ToolType::MOUSE}},
374 AMOTION_EVENT_ACTION_MOVE},
375 InputEventEntry{25ms,
376 {Pointer{.id = 0,
377 .x = 35.0f,
378 .y = 30.0f,
379 .toolType = ToolType::MOUSE,
380 .isResampled = true}},
381 AMOTION_EVENT_ACTION_MOVE}});
382
383 mClientTestChannel->assertFinishMessage(/*seq=*/1, /*handled=*/true);
384 mClientTestChannel->assertFinishMessage(/*seq=*/2, /*handled=*/true);
385 mClientTestChannel->assertFinishMessage(/*seq=*/3, /*handled=*/true);
386}
387
388/**
389 * Motion events with palm tool type are not resampled.
390 */
391TEST_F(InputConsumerResamplingTest, PalmEventIsNotResampled) {
Paul Ramirez7f1efed2024-09-29 23:55:23 +0000392 // Send the initial ACTION_DOWN separately, so that the first consumed event will only return an
Paul Ramirez5d59a422024-10-01 15:59:16 +0000393 // InputEvent with a single action.
394 mClientTestChannel->enqueueMessage(nextPointerMessage(
395 {0ms,
396 {Pointer{.id = 0, .x = 10.0f, .y = 20.0f, .toolType = ToolType::PALM}},
397 AMOTION_EVENT_ACTION_DOWN}));
398
399 mClientTestChannel->assertNoSentMessages();
400
401 invokeLooperCallback();
402 assertReceivedMotionEvent(
403 {InputEventEntry{0ms,
404 {Pointer{.id = 0, .x = 10.0f, .y = 20.0f, .toolType = ToolType::PALM}},
405 AMOTION_EVENT_ACTION_DOWN}});
406
407 // Two ACTION_MOVE events 10 ms apart that move in X direction and stay still in Y
408 mClientTestChannel->enqueueMessage(nextPointerMessage(
409 {10ms,
410 {Pointer{.id = 0, .x = 20.0f, .y = 30.0f, .toolType = ToolType::PALM}},
411 AMOTION_EVENT_ACTION_MOVE}));
412 mClientTestChannel->enqueueMessage(nextPointerMessage(
413 {20ms,
414 {Pointer{.id = 0, .x = 30.0f, .y = 30.0f, .toolType = ToolType::PALM}},
415 AMOTION_EVENT_ACTION_MOVE}));
416
417 invokeLooperCallback();
418 mConsumer->consumeBatchedInputEvents(nanoseconds{35ms}.count());
419 assertReceivedMotionEvent(
420 {InputEventEntry{10ms,
421 {Pointer{.id = 0, .x = 20.0f, .y = 30.0f, .toolType = ToolType::PALM}},
422 AMOTION_EVENT_ACTION_MOVE},
423 InputEventEntry{20ms,
424 {Pointer{.id = 0, .x = 30.0f, .y = 30.0f, .toolType = ToolType::PALM}},
425 AMOTION_EVENT_ACTION_MOVE}});
426
427 mClientTestChannel->assertFinishMessage(/*seq=*/1, /*handled=*/true);
428 mClientTestChannel->assertFinishMessage(/*seq=*/2, /*handled=*/true);
429 mClientTestChannel->assertFinishMessage(/*seq=*/3, /*handled=*/true);
430}
431
Paul Ramirez7f1efed2024-09-29 23:55:23 +0000432/**
433 * Event should not be resampled when sample time is equal to event time.
434 */
435TEST_F(InputConsumerResamplingTest, SampleTimeEqualsEventTime) {
436 // Send the initial ACTION_DOWN separately, so that the first consumed event will only return an
437 // InputEvent with a single action.
438 mClientTestChannel->enqueueMessage(nextPointerMessage(
439 {0ms, {Pointer{.id = 0, .x = 10.0f, .y = 20.0f}}, AMOTION_EVENT_ACTION_DOWN}));
440
441 mClientTestChannel->assertNoSentMessages();
442
443 invokeLooperCallback();
444 assertReceivedMotionEvent({InputEventEntry{0ms,
445 {Pointer{.id = 0, .x = 10.0f, .y = 20.0f}},
446 AMOTION_EVENT_ACTION_DOWN}});
447
448 // Two ACTION_MOVE events 10 ms apart that move in X direction and stay still in Y
449 mClientTestChannel->enqueueMessage(nextPointerMessage(
450 {10ms, {Pointer{.id = 0, .x = 20.0f, .y = 30.0f}}, AMOTION_EVENT_ACTION_MOVE}));
451 mClientTestChannel->enqueueMessage(nextPointerMessage(
452 {20ms, {Pointer{.id = 0, .x = 30.0f, .y = 30.0f}}, AMOTION_EVENT_ACTION_MOVE}));
453
454 invokeLooperCallback();
455 mConsumer->consumeBatchedInputEvents(nanoseconds{20ms + 5ms /*RESAMPLE_LATENCY*/}.count());
456
457 // MotionEvent should not resampled because the resample time falls exactly on the existing
458 // event time.
459 assertReceivedMotionEvent({InputEventEntry{10ms,
460 {Pointer{.id = 0, .x = 20.0f, .y = 30.0f}},
461 AMOTION_EVENT_ACTION_MOVE},
462 InputEventEntry{20ms,
463 {Pointer{.id = 0, .x = 30.0f, .y = 30.0f}},
464 AMOTION_EVENT_ACTION_MOVE}});
465
466 mClientTestChannel->assertFinishMessage(/*seq=*/1, /*handled=*/true);
467 mClientTestChannel->assertFinishMessage(/*seq=*/2, /*handled=*/true);
468 mClientTestChannel->assertFinishMessage(/*seq=*/3, /*handled=*/true);
469}
470
Paul Ramirez5d59a422024-10-01 15:59:16 +0000471} // namespace android