1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ui/events/ozone/evdev/touch_filter/neural_stylus_palm_detection_filter_util.h"
6 
7 #include <algorithm>
8 #include <cstdint>
9 #include <utility>
10 #include <vector>
11 
12 #include "testing/gmock/include/gmock/gmock.h"
13 #include "testing/gtest/include/gtest/gtest.h"
14 #if defined(__ANDROID__) || defined(__ANDROID_HOST__)
15 #include <linux/input-event-codes.h>
16 #include "chrome_to_android_compatibility.h"
17 #else
18 #include "ui/events/ozone/evdev/event_device_test_util.h"
19 #endif
20 #include "ui/events/ozone/evdev/touch_evdev_types.h"
21 #include "ui/events/ozone/evdev/touch_filter/palm_detection_filter.h"
22 #include "ui/events/ozone/evdev/touch_filter/shared_palm_detection_filter_state.h"
23 
24 using ::testing::ElementsAre;
25 using ::testing::IsEmpty;
26 
27 namespace ui {
28 
29 #if defined(__ANDROID__) || defined(__ANDROID_HOST__)
30 /**
31  * The tests that require an actual device (something that responds to ioctls)
32  * have been removed. The rest of the tests were simplified by modifying the
33  * 'CreatePalmFilterDeviceInfo' method.
34  */
35 enum DeviceType {
36   kNocturneTouchScreen,
37   kLinkTouchscreen,
38   kKohakuTouchscreen,
39 };
40 
CreatePalmFilterDeviceInfo(DeviceType deviceType)41 static PalmFilterDeviceInfo CreatePalmFilterDeviceInfo(DeviceType deviceType) {
42   // Default value for resolution is 1 if it's not specified (or set to 0). See
43   // https://source.chromium.org/chromium/chromium/src/+/main:ui/events/ozone/evdev/touch_filter/
44   // neural_stylus_palm_detection_filter_util.cc;l=11;drc=7f74e1d22e2d0b91856ea6b3619098cd05ef6158
45 
46   switch (deviceType) {
47     case kNocturneTouchScreen:
48       return PalmFilterDeviceInfo{
49           .max_x = 10404,
50           .max_y = 6936,
51           .x_res = 40,
52           .y_res = 40,
53           .major_radius_res = 1,
54           .minor_radius_res = 1,
55           .minor_radius_supported = true,
56       };
57     case kLinkTouchscreen:
58       return PalmFilterDeviceInfo{
59           .max_x = 2559,
60           .max_y = 1699,
61           .x_res = 20,
62           .y_res = 20,
63           .major_radius_res = 1,
64           .minor_radius_res = 1,
65           .minor_radius_supported = false,
66       };
67     case kKohakuTouchscreen:
68       return PalmFilterDeviceInfo{
69           .max_x = 2559,
70           .max_y = 1699,
71           .x_res = 1,
72           .y_res = 1,
73           .major_radius_res = 1,
74           .minor_radius_res = 1,
75           .minor_radius_supported = false,
76       };
77   }
78 }
79 
80 class EventDeviceInfo {
81  public:
GetAbsResolution(int)82   float GetAbsResolution(int /*axis*/) { return 20; }
83 };
84 
CapabilitiesToDeviceInfo(DeviceType,EventDeviceInfo *)85 bool CapabilitiesToDeviceInfo(DeviceType, EventDeviceInfo*) {
86   return true;
87 }
88 #endif
89 
90 MATCHER_P(SampleTime, time, "Does the sample have given time.") {
91   *result_listener << "Sample time" << arg.time << " is not " << time;
92   return time == arg.time;
93 }
94 
95 class NeuralStylusPalmDetectionFilterUtilTest
96     : public testing::TestWithParam<bool> {
97  public:
98   NeuralStylusPalmDetectionFilterUtilTest() = default;
99 
100   NeuralStylusPalmDetectionFilterUtilTest(
101       const NeuralStylusPalmDetectionFilterUtilTest&) = delete;
102   NeuralStylusPalmDetectionFilterUtilTest& operator=(
103       const NeuralStylusPalmDetectionFilterUtilTest&) = delete;
104 
SetUp()105   void SetUp() override {
106     EXPECT_TRUE(
107         CapabilitiesToDeviceInfo(kNocturneTouchScreen, &nocturne_touchscreen_));
108     touch_.major = 25;
109     touch_.minor = 24;
110     touch_.pressure = 23;
111     touch_.tracking_id = 22;
112     touch_.x = 21;
113     touch_.y = 20;
114     model_config_.max_sample_count = 3;
115     const bool resample_touch = GetParam();
116     if (resample_touch) {
117       model_config_.resample_period = base::Milliseconds(8);
118     }
119   }
120 
121  protected:
122   InProgressTouchEvdev touch_;
123   EventDeviceInfo nocturne_touchscreen_;
124   NeuralStylusPalmDetectionFilterModelConfig model_config_;
125 };
126 
127 INSTANTIATE_TEST_SUITE_P(ParametricUtilTest,
128                          NeuralStylusPalmDetectionFilterUtilTest,
129                          ::testing::Bool(),
__anon8fbd2f5b0102(const auto& paramInfo) 130                          [](const auto& paramInfo) {
131                            return paramInfo.param ? "ResamplingEnabled"
132                                                   : "ResamplingDisabled";
133                          });
134 
135 #if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
TEST_P(NeuralStylusPalmDetectionFilterUtilTest,DistilledNocturneTest)136 TEST_P(NeuralStylusPalmDetectionFilterUtilTest, DistilledNocturneTest) {
137   const PalmFilterDeviceInfo nocturne_distilled =
138       CreatePalmFilterDeviceInfo(nocturne_touchscreen_);
139   EXPECT_FLOAT_EQ(nocturne_distilled.max_x,
140                   nocturne_touchscreen_.GetAbsMaximum(ABS_MT_POSITION_X));
141   EXPECT_FLOAT_EQ(nocturne_distilled.max_y,
142                   nocturne_touchscreen_.GetAbsMaximum(ABS_MT_POSITION_Y));
143   EXPECT_FLOAT_EQ(nocturne_distilled.x_res,
144                   nocturne_touchscreen_.GetAbsResolution(ABS_MT_POSITION_X));
145   EXPECT_FLOAT_EQ(nocturne_distilled.y_res,
146                   nocturne_touchscreen_.GetAbsResolution(ABS_MT_POSITION_Y));
147   EXPECT_FLOAT_EQ(nocturne_distilled.major_radius_res,
148                   nocturne_touchscreen_.GetAbsResolution(ABS_MT_TOUCH_MAJOR));
149   EXPECT_TRUE(nocturne_distilled.minor_radius_supported);
150   EXPECT_FLOAT_EQ(nocturne_distilled.minor_radius_res,
151                   nocturne_touchscreen_.GetAbsResolution(ABS_MT_TOUCH_MINOR));
152 }
153 
TEST_P(NeuralStylusPalmDetectionFilterUtilTest,NoMinorResTest)154 TEST_P(NeuralStylusPalmDetectionFilterUtilTest, NoMinorResTest) {
155   // Nocturne has minor resolution, but let's pretend it doesn't. we should
156   // recover "1" as the resolution.
157   auto abs_info = nocturne_touchscreen_.GetAbsInfoByCode(ABS_MT_TOUCH_MINOR);
158   abs_info.resolution = 0;
159   nocturne_touchscreen_.SetAbsInfo(ABS_MT_TOUCH_MINOR, abs_info);
160   const PalmFilterDeviceInfo nocturne_distilled =
161       CreatePalmFilterDeviceInfo(nocturne_touchscreen_);
162   EXPECT_EQ(1, nocturne_distilled.minor_radius_res);
163   EXPECT_EQ(1, nocturne_distilled.major_radius_res);
164 }
165 #endif
166 
TEST_P(NeuralStylusPalmDetectionFilterUtilTest,DistillerKohakuTest)167 TEST_P(NeuralStylusPalmDetectionFilterUtilTest, DistillerKohakuTest) {
168   EventDeviceInfo kohaku_touchscreen;
169   ASSERT_TRUE(
170       CapabilitiesToDeviceInfo(kKohakuTouchscreen, &kohaku_touchscreen));
171   const PalmFilterDeviceInfo kohaku_distilled =
172 #if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
173       CreatePalmFilterDeviceInfo(kohaku_touchscreen);
174 #else
175       CreatePalmFilterDeviceInfo(kKohakuTouchscreen);
176 #endif
177   EXPECT_FALSE(kohaku_distilled.minor_radius_supported);
178   EXPECT_EQ(1, kohaku_distilled.x_res);
179   EXPECT_EQ(1, kohaku_distilled.y_res);
180 }
181 
TEST_P(NeuralStylusPalmDetectionFilterUtilTest,DistilledLinkTest)182 TEST_P(NeuralStylusPalmDetectionFilterUtilTest, DistilledLinkTest) {
183   EventDeviceInfo link_touchscreen;
184   ASSERT_TRUE(CapabilitiesToDeviceInfo(kLinkTouchscreen, &link_touchscreen));
185   const PalmFilterDeviceInfo link_distilled =
186 #if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
187       CreatePalmFilterDeviceInfo(link_touchscreen);
188 #else
189       CreatePalmFilterDeviceInfo(kLinkTouchscreen);
190 #endif
191   EXPECT_FALSE(link_distilled.minor_radius_supported);
192   EXPECT_FLOAT_EQ(1.f, link_distilled.major_radius_res);
193   EXPECT_FLOAT_EQ(link_distilled.major_radius_res,
194                   link_distilled.minor_radius_res);
195 }
196 
TEST_P(NeuralStylusPalmDetectionFilterUtilTest,PalmFilterSampleTest)197 TEST_P(NeuralStylusPalmDetectionFilterUtilTest, PalmFilterSampleTest) {
198   base::TimeTicks time = base::TimeTicks() + base::Seconds(30);
199   const PalmFilterDeviceInfo nocturne_distilled =
200 #if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
201       CreatePalmFilterDeviceInfo(nocturne_touchscreen_);
202 #else
203       CreatePalmFilterDeviceInfo(kNocturneTouchScreen);
204 #endif
205   const PalmFilterSample sample =
206       CreatePalmFilterSample(touch_, time, model_config_, nocturne_distilled);
207   EXPECT_EQ(time, sample.time);
208   EXPECT_EQ(25, sample.major_radius);
209   EXPECT_EQ(24, sample.minor_radius);
210   EXPECT_EQ(23, sample.pressure);
211   EXPECT_EQ(22, sample.tracking_id);
212   EXPECT_EQ(gfx::PointF(21 / 40.f, 20 / 40.f), sample.point);
213   EXPECT_EQ(0.5, sample.edge);
214 }
215 
TEST_P(NeuralStylusPalmDetectionFilterUtilTest,LinkTouchscreenSampleTest)216 TEST_P(NeuralStylusPalmDetectionFilterUtilTest, LinkTouchscreenSampleTest) {
217   EventDeviceInfo link_touchscreen;
218   base::TimeTicks time = base::TimeTicks() + base::Seconds(30);
219   ASSERT_TRUE(CapabilitiesToDeviceInfo(kLinkTouchscreen, &link_touchscreen));
220   const PalmFilterDeviceInfo link_distilled =
221 #if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
222       CreatePalmFilterDeviceInfo(link_touchscreen);
223 #else
224       CreatePalmFilterDeviceInfo(kLinkTouchscreen);
225 #endif
226   touch_.minor = 0;  // no minor from link.
227   // use 40 as a base since model is trained on that kind of device.
228   model_config_.radius_polynomial_resize = {
229       link_touchscreen.GetAbsResolution(ABS_MT_POSITION_X) / 40.0f, 0.0};
230   const PalmFilterSample sample =
231       CreatePalmFilterSample(touch_, time, model_config_, link_distilled);
232   EXPECT_FLOAT_EQ(12.5, sample.major_radius);
233   EXPECT_FLOAT_EQ(12.5, sample.minor_radius);
234 }
235 
TEST_P(NeuralStylusPalmDetectionFilterUtilTest,PalmFilterStrokeTest)236 TEST_P(NeuralStylusPalmDetectionFilterUtilTest, PalmFilterStrokeTest) {
237   PalmFilterStroke stroke(model_config_, /*tracking_id*/ 55);
238   touch_.tracking_id = 55;
239   // With no points, center is 0.
240   EXPECT_EQ(gfx::PointF(0., 0.), stroke.GetCentroid());
241 
242   base::TimeTicks time = base::TimeTicks() + base::Seconds(30);
243   const PalmFilterDeviceInfo nocturne_distilled =
244 #if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
245       CreatePalmFilterDeviceInfo(nocturne_touchscreen_);
246 #else
247       CreatePalmFilterDeviceInfo(kNocturneTouchScreen);
248 #endif
249   // Deliberately long test to ensure floating point continued accuracy.
250   for (int i = 0; i < 500000; ++i) {
251     touch_.x = 15 + i;
252     PalmFilterSample sample =
253         CreatePalmFilterSample(touch_, time, model_config_, nocturne_distilled);
254     time += base::Milliseconds(8);
255     stroke.ProcessSample(std::move(sample));
256     EXPECT_EQ(touch_.tracking_id, stroke.tracking_id());
257     if (i < 3) {
258       if (i == 0) {
259         EXPECT_FLOAT_EQ(gfx::PointF(15 / 40.f, 0.5).x(),
260                         stroke.GetCentroid().x());
261       } else if (i == 1) {
262         EXPECT_FLOAT_EQ(gfx::PointF((30 + 1) / (2 * 40.f), 0.5).x(),
263                         stroke.GetCentroid().x());
264       } else if (i == 2) {
265         EXPECT_FLOAT_EQ(gfx::PointF((45 + 1 + 2) / (3 * 40.f), 0.5).x(),
266                         stroke.GetCentroid().x());
267       }
268       continue;
269     }
270     float expected_x = (45 + 3 * i - 3) / (3 * 40.f);
271     gfx::PointF expected_centroid = gfx::PointF(expected_x, 0.5);
272     ASSERT_FLOAT_EQ(expected_centroid.x(), stroke.GetCentroid().x())
273         << "failed at i " << i;
274   }
275 }
276 
TEST_P(NeuralStylusPalmDetectionFilterUtilTest,PalmFilterStrokeBiggestSizeTest)277 TEST_P(NeuralStylusPalmDetectionFilterUtilTest,
278        PalmFilterStrokeBiggestSizeTest) {
279   PalmFilterStroke stroke(model_config_, /*tracking_id*/ 0);
280   PalmFilterStroke no_minor_stroke(model_config_, /*tracking_id*/ 0);
281   touch_.tracking_id = stroke.tracking_id();
282   EXPECT_EQ(0, stroke.BiggestSize());
283 
284   base::TimeTicks time = base::TimeTicks() + base::Seconds(30);
285   const PalmFilterDeviceInfo nocturne_distilled =
286 #if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
287       CreatePalmFilterDeviceInfo(nocturne_touchscreen_);
288 #else
289       CreatePalmFilterDeviceInfo(kNocturneTouchScreen);
290 #endif
291   for (int i = 0; i < 500; ++i) {
292     touch_.major = 2 + i;
293     touch_.minor = 1 + i;
294     PalmFilterSample sample =
295         CreatePalmFilterSample(touch_, time, model_config_, nocturne_distilled);
296     EXPECT_EQ(static_cast<uint64_t>(i), stroke.samples_seen());
297     stroke.ProcessSample(sample);
298     EXPECT_FLOAT_EQ((1 + i) * (2 + i), stroke.BiggestSize());
299 
300     PalmFilterSample second_sample =
301         CreatePalmFilterSample(touch_, time, model_config_, nocturne_distilled);
302     second_sample.minor_radius = 0;
303     no_minor_stroke.ProcessSample(std::move(second_sample));
304     EXPECT_FLOAT_EQ((2 + i) * (2 + i), no_minor_stroke.BiggestSize());
305     ASSERT_EQ(std::min(3ul, 1ul + i), stroke.samples().size());
306     time += base::Milliseconds(8);
307   }
308 }
309 
TEST_P(NeuralStylusPalmDetectionFilterUtilTest,UnscaledMajorMinorResolution)310 TEST_P(NeuralStylusPalmDetectionFilterUtilTest, UnscaledMajorMinorResolution) {
311   model_config_.radius_polynomial_resize = {};
312   PalmFilterDeviceInfo device_info;
313   device_info.x_res = 2;
314   device_info.y_res = 5;
315   device_info.major_radius_res = 2;
316   device_info.minor_radius_res = 5;
317   device_info.minor_radius_supported = true;
318   touch_.major = 20;
319   touch_.minor = 10;
320   touch_.orientation = 0;
321   base::TimeTicks time = base::TimeTicks::UnixEpoch() + base::Seconds(30);
322   PalmFilterSample sample =
323       CreatePalmFilterSample(touch_, time, model_config_, device_info);
324   EXPECT_EQ(20 / 2, sample.major_radius);
325   EXPECT_EQ(10 / 5, sample.minor_radius);
326 }
327 
TEST_P(NeuralStylusPalmDetectionFilterUtilTest,StrokeGetMaxMajorTest)328 TEST_P(NeuralStylusPalmDetectionFilterUtilTest, StrokeGetMaxMajorTest) {
329   PalmFilterStroke stroke(model_config_, /*tracking_id*/ 0);
330   touch_.tracking_id = stroke.tracking_id();
331   EXPECT_FLOAT_EQ(0, stroke.MaxMajorRadius());
332   base::TimeTicks time = base::TimeTicks::UnixEpoch() + base::Seconds(30);
333   const PalmFilterDeviceInfo nocturne_distilled =
334 #if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
335       CreatePalmFilterDeviceInfo(nocturne_touchscreen_);
336 #else
337       CreatePalmFilterDeviceInfo(kNocturneTouchScreen);
338 #endif
339   for (int i = 1; i < 50; ++i) {
340     touch_.major = i;
341     touch_.minor = i - 1;
342     PalmFilterSample sample =
343         CreatePalmFilterSample(touch_, time, model_config_, nocturne_distilled);
344     time += base::Milliseconds(8);
345     EXPECT_EQ(static_cast<uint64_t>(i - 1), stroke.samples_seen());
346     stroke.ProcessSample(sample);
347     EXPECT_FLOAT_EQ(i, stroke.MaxMajorRadius());
348   }
349 }
350 
TEST_P(NeuralStylusPalmDetectionFilterUtilTest,SampleRadiusConversion)351 TEST_P(NeuralStylusPalmDetectionFilterUtilTest, SampleRadiusConversion) {
352   // A single number: a _constant_.
353   model_config_.radius_polynomial_resize = {71.3};
354   base::TimeTicks time = base::TimeTicks::UnixEpoch() + base::Seconds(30);
355   const PalmFilterDeviceInfo nocturne_distilled =
356 #if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
357       CreatePalmFilterDeviceInfo(nocturne_touchscreen_);
358 #else
359       CreatePalmFilterDeviceInfo(kNocturneTouchScreen);
360 #endif
361   PalmFilterSample sample =
362       CreatePalmFilterSample(touch_, time, model_config_, nocturne_distilled);
363   EXPECT_FLOAT_EQ(71.3, sample.major_radius);
364   EXPECT_FLOAT_EQ(71.3, sample.minor_radius);
365 
366   // 0.1*r^2 + 0.4*r - 5.0
367   model_config_.radius_polynomial_resize = {0.1, 0.4, -5.0};
368   sample =
369       CreatePalmFilterSample(touch_, time, model_config_, nocturne_distilled);
370   EXPECT_FLOAT_EQ(0.1 * 25 * 25 + 0.4 * 25 - 5.0, sample.major_radius);
371   EXPECT_FLOAT_EQ(0.1 * 24 * 24 + 0.4 * 24 - 5.0, sample.minor_radius);
372 }
373 
TEST(PalmFilterStrokeTest,NumberOfResampledValues)374 TEST(PalmFilterStrokeTest, NumberOfResampledValues) {
375   NeuralStylusPalmDetectionFilterModelConfig model_config_;
376   model_config_.max_sample_count = 3;
377   model_config_.resample_period = base::Milliseconds(8);
378   base::TimeTicks down_time = base::TimeTicks::UnixEpoch() + base::Seconds(30);
379 
380   PalmFilterStroke stroke(model_config_, /*tracking_id*/ 0);
381   const PalmFilterDeviceInfo device_info;
382 
383   // Initially, no samples
384   ASSERT_THAT(stroke.samples(), IsEmpty());
385   ASSERT_EQ(0u, stroke.samples_seen());
386 
387   // Add first sample at time = T
388   InProgressTouchEvdev touch_;
389   touch_.tracking_id = stroke.tracking_id();
390   PalmFilterSample sample =
391       CreatePalmFilterSample(touch_, down_time, model_config_, device_info);
392   stroke.ProcessSample(sample);
393   ASSERT_THAT(stroke.samples(), ElementsAre(SampleTime(down_time)));
394   ASSERT_EQ(1u, stroke.samples_seen());
395 
396   // Add second sample at time = T + 4ms. All samples are stored, even if it's
397   // before the next resample time.
398   base::TimeTicks time = down_time + base::Milliseconds(4);
399   sample = CreatePalmFilterSample(touch_, time, model_config_, device_info);
400   stroke.ProcessSample(sample);
401   ASSERT_THAT(stroke.samples(),
402               ElementsAre(SampleTime(down_time), SampleTime(time)));
403   ASSERT_EQ(2u, stroke.samples_seen());
404 
405   // Add third sample at time = T + 10ms.
406   time = down_time + base::Milliseconds(10);
407   sample = CreatePalmFilterSample(touch_, time, model_config_, device_info);
408   stroke.ProcessSample(sample);
409   ASSERT_THAT(stroke.samples(),
410               ElementsAre(SampleTime(down_time),
411                           SampleTime(down_time + base::Milliseconds(4)),
412                           SampleTime(down_time + base::Milliseconds(10))));
413   ASSERT_EQ(3u, stroke.samples_seen());
414 }
415 
TEST(PalmFilterStrokeTest,ResamplingTest)416 TEST(PalmFilterStrokeTest, ResamplingTest) {
417   NeuralStylusPalmDetectionFilterModelConfig model_config_;
418   model_config_.max_sample_count = 3;
419   model_config_.resample_period = base::Milliseconds(8);
420 
421   PalmFilterStroke stroke(model_config_, /*tracking_id*/ 0);
422   PalmFilterDeviceInfo device_info;
423   device_info.minor_radius_supported = true;
424 
425   // Add first sample at time = T
426   InProgressTouchEvdev touch_;
427   touch_.tracking_id = stroke.tracking_id();
428   touch_.x = 1;
429   touch_.y = 2;
430   touch_.major = 4;
431   touch_.minor = 3;
432   base::TimeTicks down_time = base::TimeTicks::UnixEpoch() + base::Seconds(30);
433   PalmFilterSample sample1 =
434       CreatePalmFilterSample(touch_, down_time, model_config_, device_info);
435   stroke.ProcessSample(sample1);
436   // First sample should not be modified
437   ASSERT_THAT(stroke.samples(), ElementsAre(sample1));
438 
439   // Add second sample at time = T + 2ms. It's not yet time for the new frame,
440   // so no new sample should be generated.
441   base::TimeTicks time = down_time + base::Milliseconds(4);
442   touch_.x = 100;
443   touch_.y = 20;
444   touch_.major = 12;
445   touch_.minor = 11;
446   PalmFilterSample sample2 =
447       CreatePalmFilterSample(touch_, time, model_config_, device_info);
448   stroke.ProcessSample(sample2);
449   // The samples should remain unchanged
450   ASSERT_THAT(stroke.samples(), ElementsAre(sample1, sample2));
451 
452   // Add third sample at time = T + 12ms. A resampled event at time = T + 8ms
453   // should be generated.
454   time = down_time + base::Milliseconds(12);
455   touch_.x = 200;
456   touch_.y = 24;
457   touch_.major = 14;
458   touch_.minor = 13;
459   PalmFilterSample sample3 =
460       CreatePalmFilterSample(touch_, time, model_config_, device_info);
461   stroke.ProcessSample(sample3);
462   ASSERT_THAT(stroke.samples(), ElementsAre(sample1, sample2, sample3));
463 }
464 
465 /**
466  * There should always be at least (max_sample_count - 1) * resample_period
467  * worth of samples. However, that's not sufficient. In the cases where the gap
468  * between samples is very large (larger than the sample horizon), there needs
469  * to be another sample in order to calculate resampled values throughout the
470  * entire range.
471  */
TEST(PalmFilterStrokeTest,MultipleResampledValues)472 TEST(PalmFilterStrokeTest, MultipleResampledValues) {
473   NeuralStylusPalmDetectionFilterModelConfig model_config_;
474   model_config_.max_sample_count = 3;
475   model_config_.resample_period = base::Milliseconds(8);
476 
477   PalmFilterStroke stroke(model_config_, /*tracking_id*/ 0);
478   PalmFilterDeviceInfo device_info;
479   device_info.minor_radius_supported = true;
480 
481   // Add first sample at time = T
482   InProgressTouchEvdev touch_;
483   touch_.tracking_id = stroke.tracking_id();
484   touch_.x = 0;
485   touch_.y = 10;
486   touch_.major = 200;
487   touch_.minor = 100;
488   base::TimeTicks down_time = base::TimeTicks::UnixEpoch() + base::Seconds(30);
489   PalmFilterSample sample1 =
490       CreatePalmFilterSample(touch_, down_time, model_config_, device_info);
491   stroke.ProcessSample(sample1);
492   // First sample should go in as is
493   ASSERT_THAT(stroke.samples(), ElementsAre(sample1));
494 
495   // Add second sample at time = T + 20ms.
496   base::TimeTicks time = down_time + base::Milliseconds(20);
497   touch_.x = 20;
498   touch_.y = 30;
499   touch_.major = 220;
500   touch_.minor = 120;
501   PalmFilterSample sample2 =
502       CreatePalmFilterSample(touch_, time, model_config_, device_info);
503   stroke.ProcessSample(sample2);
504 
505   ASSERT_THAT(stroke.samples(), ElementsAre(sample1, sample2));
506 
507   // Verify resampled sample : time = T + 8ms
508   PalmFilterSample resampled_sample =
509       stroke.GetSampleAt(down_time + base::Milliseconds(8));
510   EXPECT_EQ(8, resampled_sample.point.x());
511   EXPECT_EQ(18, resampled_sample.point.y());
512   EXPECT_EQ(220, resampled_sample.major_radius);
513   EXPECT_EQ(120, resampled_sample.minor_radius);
514 
515   // Verify resampled sample : time = T + 16ms
516   resampled_sample = stroke.GetSampleAt(down_time + base::Milliseconds(16));
517   EXPECT_EQ(16, resampled_sample.point.x());
518   EXPECT_EQ(26, resampled_sample.point.y());
519   EXPECT_EQ(220, resampled_sample.major_radius);
520   EXPECT_EQ(120, resampled_sample.minor_radius);
521 }
522 
523 }  // namespace ui
524