blob: 90f509d97f7bbf72596224c0d702584c73766143 [file] [log] [blame]
Prabir Pradhan0762b1f2023-06-22 23:08:18 +00001/*
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//! Common definitions of the Android Input Framework in rust.
18
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000019use crate::ffi::RustInputDeviceIdentifier;
Prabir Pradhan0762b1f2023-06-22 23:08:18 +000020use bitflags::bitflags;
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000021use inputconstants::aidl::android::os::IInputConstants;
Prabir Pradhan29814f22024-06-18 14:53:10 +000022use inputconstants::aidl::android::os::MotionEventFlag::MotionEventFlag;
Vaibhav Devmurari099fb592024-06-26 14:26:30 +000023use serde::{Deserialize, Serialize};
Prabir Pradhan0762b1f2023-06-22 23:08:18 +000024use std::fmt;
25
26/// The InputDevice ID.
27#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
28pub struct DeviceId(pub i32);
29
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000030/// The InputDevice equivalent for Rust inputflinger
31#[derive(Debug)]
32pub struct InputDevice {
33 /// InputDevice ID
34 pub device_id: DeviceId,
35 /// InputDevice unique identifier
36 pub identifier: RustInputDeviceIdentifier,
37 /// InputDevice classes (equivalent to EventHub InputDeviceClass)
38 pub classes: DeviceClass,
39}
40
Siarhei Vishniakou2d151ac2023-09-19 13:30:24 -070041#[repr(u32)]
42pub enum SourceClass {
43 None = input_bindgen::AINPUT_SOURCE_CLASS_NONE,
44 Button = input_bindgen::AINPUT_SOURCE_CLASS_BUTTON,
45 Pointer = input_bindgen::AINPUT_SOURCE_CLASS_POINTER,
46 Navigation = input_bindgen::AINPUT_SOURCE_CLASS_NAVIGATION,
47 Position = input_bindgen::AINPUT_SOURCE_CLASS_POSITION,
48 Joystick = input_bindgen::AINPUT_SOURCE_CLASS_JOYSTICK,
49}
50
51bitflags! {
52 /// Source of the input device or input events.
Siarhei Vishniakou88daa902023-10-03 14:04:18 -070053 #[derive(Debug, PartialEq)]
Siarhei Vishniakou2d151ac2023-09-19 13:30:24 -070054 pub struct Source: u32 {
Siarhei Vishniakou88daa902023-10-03 14:04:18 -070055 // Constants from SourceClass, added here for compatibility reasons
56 /// SourceClass::Button
57 const SourceClassButton = SourceClass::Button as u32;
58 /// SourceClass::Pointer
59 const SourceClassPointer = SourceClass::Pointer as u32;
60 /// SourceClass::Navigation
61 const SourceClassNavigation = SourceClass::Navigation as u32;
62 /// SourceClass::Position
63 const SourceClassPosition = SourceClass::Position as u32;
64 /// SourceClass::Joystick
65 const SourceClassJoystick = SourceClass::Joystick as u32;
66
Siarhei Vishniakou2d151ac2023-09-19 13:30:24 -070067 /// SOURCE_UNKNOWN
68 const Unknown = input_bindgen::AINPUT_SOURCE_UNKNOWN;
69 /// SOURCE_KEYBOARD
70 const Keyboard = input_bindgen::AINPUT_SOURCE_KEYBOARD;
71 /// SOURCE_DPAD
72 const Dpad = input_bindgen::AINPUT_SOURCE_DPAD;
73 /// SOURCE_GAMEPAD
74 const Gamepad = input_bindgen::AINPUT_SOURCE_GAMEPAD;
75 /// SOURCE_TOUCHSCREEN
76 const Touchscreen = input_bindgen::AINPUT_SOURCE_TOUCHSCREEN;
77 /// SOURCE_MOUSE
78 const Mouse = input_bindgen::AINPUT_SOURCE_MOUSE;
79 /// SOURCE_STYLUS
80 const Stylus = input_bindgen::AINPUT_SOURCE_STYLUS;
81 /// SOURCE_BLUETOOTH_STYLUS
82 const BluetoothStylus = input_bindgen::AINPUT_SOURCE_BLUETOOTH_STYLUS;
83 /// SOURCE_TRACKBALL
84 const Trackball = input_bindgen::AINPUT_SOURCE_TRACKBALL;
85 /// SOURCE_MOUSE_RELATIVE
86 const MouseRelative = input_bindgen::AINPUT_SOURCE_MOUSE_RELATIVE;
87 /// SOURCE_TOUCHPAD
88 const Touchpad = input_bindgen::AINPUT_SOURCE_TOUCHPAD;
89 /// SOURCE_TOUCH_NAVIGATION
90 const TouchNavigation = input_bindgen::AINPUT_SOURCE_TOUCH_NAVIGATION;
91 /// SOURCE_JOYSTICK
92 const Joystick = input_bindgen::AINPUT_SOURCE_JOYSTICK;
93 /// SOURCE_HDMI
94 const HDMI = input_bindgen::AINPUT_SOURCE_HDMI;
95 /// SOURCE_SENSOR
96 const Sensor = input_bindgen::AINPUT_SOURCE_SENSOR;
97 /// SOURCE_ROTARY_ENCODER
98 const RotaryEncoder = input_bindgen::AINPUT_SOURCE_ROTARY_ENCODER;
99 }
100}
101
Prabir Pradhan0762b1f2023-06-22 23:08:18 +0000102/// A rust enum representation of a MotionEvent action.
103#[repr(u32)]
104pub enum MotionAction {
105 /// ACTION_DOWN
106 Down = input_bindgen::AMOTION_EVENT_ACTION_DOWN,
107 /// ACTION_UP
108 Up = input_bindgen::AMOTION_EVENT_ACTION_UP,
109 /// ACTION_MOVE
110 Move = input_bindgen::AMOTION_EVENT_ACTION_MOVE,
111 /// ACTION_CANCEL
112 Cancel = input_bindgen::AMOTION_EVENT_ACTION_CANCEL,
113 /// ACTION_OUTSIDE
114 Outside = input_bindgen::AMOTION_EVENT_ACTION_OUTSIDE,
115 /// ACTION_POINTER_DOWN
116 PointerDown {
117 /// The index of the affected pointer.
118 action_index: usize,
119 } = input_bindgen::AMOTION_EVENT_ACTION_POINTER_DOWN,
120 /// ACTION_POINTER_UP
121 PointerUp {
122 /// The index of the affected pointer.
123 action_index: usize,
124 } = input_bindgen::AMOTION_EVENT_ACTION_POINTER_UP,
125 /// ACTION_HOVER_ENTER
126 HoverEnter = input_bindgen::AMOTION_EVENT_ACTION_HOVER_ENTER,
127 /// ACTION_HOVER_MOVE
128 HoverMove = input_bindgen::AMOTION_EVENT_ACTION_HOVER_MOVE,
129 /// ACTION_HOVER_EXIT
130 HoverExit = input_bindgen::AMOTION_EVENT_ACTION_HOVER_EXIT,
131 /// ACTION_SCROLL
132 Scroll = input_bindgen::AMOTION_EVENT_ACTION_SCROLL,
133 /// ACTION_BUTTON_PRESS
134 ButtonPress = input_bindgen::AMOTION_EVENT_ACTION_BUTTON_PRESS,
135 /// ACTION_BUTTON_RELEASE
136 ButtonRelease = input_bindgen::AMOTION_EVENT_ACTION_BUTTON_RELEASE,
137}
138
139impl fmt::Display for MotionAction {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 match self {
142 MotionAction::Down => write!(f, "DOWN"),
143 MotionAction::Up => write!(f, "UP"),
144 MotionAction::Move => write!(f, "MOVE"),
145 MotionAction::Cancel => write!(f, "CANCEL"),
146 MotionAction::Outside => write!(f, "OUTSIDE"),
147 MotionAction::PointerDown { action_index } => {
148 write!(f, "POINTER_DOWN({})", action_index)
149 }
150 MotionAction::PointerUp { action_index } => write!(f, "POINTER_UP({})", action_index),
151 MotionAction::HoverMove => write!(f, "HOVER_MOVE"),
152 MotionAction::Scroll => write!(f, "SCROLL"),
153 MotionAction::HoverEnter => write!(f, "HOVER_ENTER"),
154 MotionAction::HoverExit => write!(f, "HOVER_EXIT"),
155 MotionAction::ButtonPress => write!(f, "BUTTON_PRESS"),
156 MotionAction::ButtonRelease => write!(f, "BUTTON_RELEASE"),
157 }
158 }
159}
160
161impl From<u32> for MotionAction {
162 fn from(action: u32) -> Self {
163 let (action_masked, action_index) = MotionAction::breakdown_action(action);
164 match action_masked {
165 input_bindgen::AMOTION_EVENT_ACTION_DOWN => MotionAction::Down,
166 input_bindgen::AMOTION_EVENT_ACTION_UP => MotionAction::Up,
167 input_bindgen::AMOTION_EVENT_ACTION_MOVE => MotionAction::Move,
168 input_bindgen::AMOTION_EVENT_ACTION_CANCEL => MotionAction::Cancel,
169 input_bindgen::AMOTION_EVENT_ACTION_OUTSIDE => MotionAction::Outside,
170 input_bindgen::AMOTION_EVENT_ACTION_POINTER_DOWN => {
171 MotionAction::PointerDown { action_index }
172 }
173 input_bindgen::AMOTION_EVENT_ACTION_POINTER_UP => {
174 MotionAction::PointerUp { action_index }
175 }
176 input_bindgen::AMOTION_EVENT_ACTION_HOVER_ENTER => MotionAction::HoverEnter,
177 input_bindgen::AMOTION_EVENT_ACTION_HOVER_MOVE => MotionAction::HoverMove,
178 input_bindgen::AMOTION_EVENT_ACTION_HOVER_EXIT => MotionAction::HoverExit,
179 input_bindgen::AMOTION_EVENT_ACTION_SCROLL => MotionAction::Scroll,
180 input_bindgen::AMOTION_EVENT_ACTION_BUTTON_PRESS => MotionAction::ButtonPress,
181 input_bindgen::AMOTION_EVENT_ACTION_BUTTON_RELEASE => MotionAction::ButtonRelease,
182 _ => panic!("Unknown action: {}", action),
183 }
184 }
185}
186
187impl MotionAction {
188 fn breakdown_action(action: u32) -> (u32, usize) {
189 let action_masked = action & input_bindgen::AMOTION_EVENT_ACTION_MASK;
190 let index = (action & input_bindgen::AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)
191 >> input_bindgen::AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
192 (action_masked, index.try_into().unwrap())
193 }
194}
195
196bitflags! {
197 /// MotionEvent flags.
Prabir Pradhan29814f22024-06-18 14:53:10 +0000198 /// The source of truth for the flag definitions are the MotionEventFlag AIDL enum.
199 /// The flag values are redefined here as a bitflags API.
Siarhei Vishniakou93992432023-10-09 15:47:48 -0700200 #[derive(Debug)]
Siarhei Vishniakou227a7f82023-07-18 18:30:32 -0700201 pub struct MotionFlags: u32 {
Siarhei Vishniakou227a7f82023-07-18 18:30:32 -0700202 /// FLAG_WINDOW_IS_OBSCURED
Prabir Pradhan29814f22024-06-18 14:53:10 +0000203 const WINDOW_IS_OBSCURED = MotionEventFlag::WINDOW_IS_OBSCURED.0 as u32;
Siarhei Vishniakou227a7f82023-07-18 18:30:32 -0700204 /// FLAG_WINDOW_IS_PARTIALLY_OBSCURED
Prabir Pradhan29814f22024-06-18 14:53:10 +0000205 const WINDOW_IS_PARTIALLY_OBSCURED = MotionEventFlag::WINDOW_IS_PARTIALLY_OBSCURED.0 as u32;
Siarhei Vishniakouc40f6e02024-04-25 15:49:29 -0700206 /// FLAG_HOVER_EXIT_PENDING
Prabir Pradhan29814f22024-06-18 14:53:10 +0000207 const HOVER_EXIT_PENDING = MotionEventFlag::HOVER_EXIT_PENDING.0 as u32;
Linnan Lia3b18f42024-04-17 16:54:55 +0000208 /// FLAG_IS_GENERATED_GESTURE
Prabir Pradhan29814f22024-06-18 14:53:10 +0000209 const IS_GENERATED_GESTURE = MotionEventFlag::IS_GENERATED_GESTURE.0 as u32;
Siarhei Vishniakouc40f6e02024-04-25 15:49:29 -0700210 /// FLAG_CANCELED
Prabir Pradhan29814f22024-06-18 14:53:10 +0000211 const CANCELED = MotionEventFlag::CANCELED.0 as u32;
Siarhei Vishniakouc40f6e02024-04-25 15:49:29 -0700212 /// FLAG_NO_FOCUS_CHANGE
Prabir Pradhan29814f22024-06-18 14:53:10 +0000213 const NO_FOCUS_CHANGE = MotionEventFlag::NO_FOCUS_CHANGE.0 as u32;
Prabir Pradhancda32662024-06-11 15:31:40 +0000214 /// PRIVATE_FLAG_SUPPORTS_ORIENTATION
Prabir Pradhan29814f22024-06-18 14:53:10 +0000215 const PRIVATE_FLAG_SUPPORTS_ORIENTATION =
216 MotionEventFlag::PRIVATE_FLAG_SUPPORTS_ORIENTATION.0 as u32;
Prabir Pradhancda32662024-06-11 15:31:40 +0000217 /// PRIVATE_FLAG_SUPPORTS_DIRECTIONAL_ORIENTATION
Prabir Pradhan29814f22024-06-18 14:53:10 +0000218 const PRIVATE_FLAG_SUPPORTS_DIRECTIONAL_ORIENTATION =
219 MotionEventFlag::PRIVATE_FLAG_SUPPORTS_DIRECTIONAL_ORIENTATION.0 as u32;
Siarhei Vishniakouc40f6e02024-04-25 15:49:29 -0700220 /// FLAG_IS_ACCESSIBILITY_EVENT
Prabir Pradhan29814f22024-06-18 14:53:10 +0000221 const IS_ACCESSIBILITY_EVENT = MotionEventFlag::IS_ACCESSIBILITY_EVENT.0 as u32;
Linnan Lif3ec5482024-04-18 16:45:05 +0000222 /// FLAG_TAINTED
Prabir Pradhan29814f22024-06-18 14:53:10 +0000223 const TAINTED = MotionEventFlag::TAINTED.0 as u32;
Siarhei Vishniakouc40f6e02024-04-25 15:49:29 -0700224 /// FLAG_TARGET_ACCESSIBILITY_FOCUS
Prabir Pradhan29814f22024-06-18 14:53:10 +0000225 const TARGET_ACCESSIBILITY_FOCUS = MotionEventFlag::TARGET_ACCESSIBILITY_FOCUS.0 as u32;
Prabir Pradhan0762b1f2023-06-22 23:08:18 +0000226 }
227}
Siarhei Vishniakou2d151ac2023-09-19 13:30:24 -0700228
229impl Source {
230 /// Return true if this source is from the provided class
231 pub fn is_from_class(&self, source_class: SourceClass) -> bool {
232 let class_bits = source_class as u32;
233 self.bits() & class_bits == class_bits
234 }
235}
Siarhei Vishniakou88daa902023-10-03 14:04:18 -0700236
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +0000237bitflags! {
238 /// Device class of the input device. These are duplicated from Eventhub.h
239 /// We need to make sure the two version remain in sync when adding new classes.
240 #[derive(Debug, PartialEq)]
241 pub struct DeviceClass: u32 {
242 /// The input device is a keyboard or has buttons
243 const Keyboard = IInputConstants::DEVICE_CLASS_KEYBOARD as u32;
244 /// The input device is an alpha-numeric keyboard (not just a dial pad)
245 const AlphabeticKey = IInputConstants::DEVICE_CLASS_ALPHAKEY as u32;
246 /// The input device is a touchscreen or a touchpad (either single-touch or multi-touch)
247 const Touch = IInputConstants::DEVICE_CLASS_TOUCH as u32;
248 /// The input device is a cursor device such as a trackball or mouse.
249 const Cursor = IInputConstants::DEVICE_CLASS_CURSOR as u32;
250 /// The input device is a multi-touch touchscreen or touchpad.
251 const MultiTouch = IInputConstants::DEVICE_CLASS_TOUCH_MT as u32;
252 /// The input device is a directional pad (implies keyboard, has DPAD keys).
253 const Dpad = IInputConstants::DEVICE_CLASS_DPAD as u32;
254 /// The input device is a gamepad (implies keyboard, has BUTTON keys).
255 const Gamepad = IInputConstants::DEVICE_CLASS_GAMEPAD as u32;
256 /// The input device has switches.
257 const Switch = IInputConstants::DEVICE_CLASS_SWITCH as u32;
258 /// The input device is a joystick (implies gamepad, has joystick absolute axes).
259 const Joystick = IInputConstants::DEVICE_CLASS_JOYSTICK as u32;
260 /// The input device has a vibrator (supports FF_RUMBLE).
261 const Vibrator = IInputConstants::DEVICE_CLASS_VIBRATOR as u32;
262 /// The input device has a microphone.
263 const Mic = IInputConstants::DEVICE_CLASS_MIC as u32;
264 /// The input device is an external stylus (has data we want to fuse with touch data).
265 const ExternalStylus = IInputConstants::DEVICE_CLASS_EXTERNAL_STYLUS as u32;
266 /// The input device has a rotary encoder
267 const RotaryEncoder = IInputConstants::DEVICE_CLASS_ROTARY_ENCODER as u32;
268 /// The input device has a sensor like accelerometer, gyro, etc
269 const Sensor = IInputConstants::DEVICE_CLASS_SENSOR as u32;
270 /// The input device has a battery
271 const Battery = IInputConstants::DEVICE_CLASS_BATTERY as u32;
272 /// The input device has sysfs controllable lights
273 const Light = IInputConstants::DEVICE_CLASS_LIGHT as u32;
274 /// The input device is a touchpad, requiring an on-screen cursor.
275 const Touchpad = IInputConstants::DEVICE_CLASS_TOUCHPAD as u32;
276 /// The input device is virtual (not a real device, not part of UI configuration).
277 const Virtual = IInputConstants::DEVICE_CLASS_VIRTUAL as u32;
278 /// The input device is external (not built-in).
279 const External = IInputConstants::DEVICE_CLASS_EXTERNAL as u32;
280 }
281}
282
283bitflags! {
284 /// Modifier state flags
Vaibhav Devmurari770b6e42024-06-10 10:29:26 +0000285 #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +0000286 pub struct ModifierState: u32 {
287 /// No meta keys are pressed
288 const None = input_bindgen::AMETA_NONE;
289 /// This mask is used to check whether one of the ALT meta keys is pressed
290 const AltOn = input_bindgen::AMETA_ALT_ON;
291 /// This mask is used to check whether the left ALT meta key is pressed
292 const AltLeftOn = input_bindgen::AMETA_ALT_LEFT_ON;
293 /// This mask is used to check whether the right ALT meta key is pressed
294 const AltRightOn = input_bindgen::AMETA_ALT_RIGHT_ON;
295 /// This mask is used to check whether one of the SHIFT meta keys is pressed
296 const ShiftOn = input_bindgen::AMETA_SHIFT_ON;
297 /// This mask is used to check whether the left SHIFT meta key is pressed
298 const ShiftLeftOn = input_bindgen::AMETA_SHIFT_LEFT_ON;
299 /// This mask is used to check whether the right SHIFT meta key is pressed
300 const ShiftRightOn = input_bindgen::AMETA_SHIFT_RIGHT_ON;
301 /// This mask is used to check whether the SYM meta key is pressed
302 const SymOn = input_bindgen::AMETA_SYM_ON;
303 /// This mask is used to check whether the FUNCTION meta key is pressed
304 const FunctionOn = input_bindgen::AMETA_FUNCTION_ON;
305 /// This mask is used to check whether one of the CTRL meta keys is pressed
306 const CtrlOn = input_bindgen::AMETA_CTRL_ON;
307 /// This mask is used to check whether the left CTRL meta key is pressed
308 const CtrlLeftOn = input_bindgen::AMETA_CTRL_LEFT_ON;
309 /// This mask is used to check whether the right CTRL meta key is pressed
310 const CtrlRightOn = input_bindgen::AMETA_CTRL_RIGHT_ON;
311 /// This mask is used to check whether one of the META meta keys is pressed
312 const MetaOn = input_bindgen::AMETA_META_ON;
313 /// This mask is used to check whether the left META meta key is pressed
314 const MetaLeftOn = input_bindgen::AMETA_META_LEFT_ON;
315 /// This mask is used to check whether the right META meta key is pressed
316 const MetaRightOn = input_bindgen::AMETA_META_RIGHT_ON;
317 /// This mask is used to check whether the CAPS LOCK meta key is on
318 const CapsLockOn = input_bindgen::AMETA_CAPS_LOCK_ON;
319 /// This mask is used to check whether the NUM LOCK meta key is on
320 const NumLockOn = input_bindgen::AMETA_NUM_LOCK_ON;
321 /// This mask is used to check whether the SCROLL LOCK meta key is on
322 const ScrollLockOn = input_bindgen::AMETA_SCROLL_LOCK_ON;
323 }
324}
325
326/// A rust enum representation of a Keyboard type.
327#[repr(u32)]
Vaibhav Devmurari099fb592024-06-26 14:26:30 +0000328#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
329#[serde(tag = "type")]
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +0000330pub enum KeyboardType {
331 /// KEYBOARD_TYPE_NONE
Vaibhav Devmurari099fb592024-06-26 14:26:30 +0000332 #[default]
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +0000333 None = input_bindgen::AINPUT_KEYBOARD_TYPE_NONE,
334 /// KEYBOARD_TYPE_NON_ALPHABETIC
335 NonAlphabetic = input_bindgen::AINPUT_KEYBOARD_TYPE_NON_ALPHABETIC,
336 /// KEYBOARD_TYPE_ALPHABETIC
337 Alphabetic = input_bindgen::AINPUT_KEYBOARD_TYPE_ALPHABETIC,
338}
339
Siarhei Vishniakou88daa902023-10-03 14:04:18 -0700340#[cfg(test)]
341mod tests {
342 use crate::input::SourceClass;
Prabir Pradhan29814f22024-06-18 14:53:10 +0000343 use crate::MotionFlags;
Siarhei Vishniakou88daa902023-10-03 14:04:18 -0700344 use crate::Source;
Prabir Pradhan29814f22024-06-18 14:53:10 +0000345 use inputconstants::aidl::android::os::MotionEventFlag::MotionEventFlag;
346
Siarhei Vishniakou88daa902023-10-03 14:04:18 -0700347 #[test]
348 fn convert_source_class_pointer() {
349 let source = Source::from_bits(input_bindgen::AINPUT_SOURCE_CLASS_POINTER).unwrap();
350 assert!(source.is_from_class(SourceClass::Pointer));
351 }
Prabir Pradhan29814f22024-06-18 14:53:10 +0000352
353 /// Ensure all MotionEventFlag constants are re-defined in rust.
354 #[test]
355 fn all_motion_event_flags_defined() {
356 for flag in MotionEventFlag::enum_values() {
357 assert!(
358 MotionFlags::from_bits(flag.0 as u32).is_some(),
359 "MotionEventFlag value {flag:?} is not redefined as a rust MotionFlags"
360 );
361 }
362 }
Siarhei Vishniakou88daa902023-10-03 14:04:18 -0700363}