1use crate::input_device::{self, InputEvent, UnhandledInputEvent};
6use crate::input_handler::{Handler, InputHandlerStatus, UnhandledInputHandler};
7use crate::keyboard_binding::{KeyboardDeviceDescriptor, KeyboardEvent};
8use crate::metrics;
9use anyhow::{Context, Result};
10use async_trait::async_trait;
11use fidl_fuchsia_ui_composition_internal as fcomp;
12use fidl_fuchsia_ui_input3::KeyEventType;
13use fuchsia_async::{OnSignals, Task};
14use fuchsia_inspect::health::Reporter;
15use futures::StreamExt;
16use futures::channel::mpsc::{self, UnboundedReceiver, UnboundedSender};
17use keymaps::KeyState;
18use metrics_registry::InputPipelineErrorMetricDimensionEvent;
19use std::cell::RefCell;
20use std::rc::Rc;
21use std::sync::LazyLock;
22use zx::{AsHandleRef, MonotonicDuration, MonotonicInstant, Signals, Status};
23
24static DISPLAY_OWNED: LazyLock<Signals> = LazyLock::new(|| {
27 Signals::from_bits(fcomp::SIGNAL_DISPLAY_OWNED).expect("static init should not fail")
28});
29
30static DISPLAY_UNOWNED: LazyLock<Signals> = LazyLock::new(|| {
33 Signals::from_bits(fcomp::SIGNAL_DISPLAY_NOT_OWNED).expect("static init should not fail")
34});
35
36static ANY_DISPLAY_EVENT: LazyLock<Signals> = LazyLock::new(|| *DISPLAY_OWNED | *DISPLAY_UNOWNED);
38
39#[derive(Debug, Clone, PartialEq)]
41struct Ownership {
42 signals: Signals,
43}
44
45impl std::convert::From<Signals> for Ownership {
46 fn from(signals: Signals) -> Self {
47 Ownership { signals }
48 }
49}
50
51impl Ownership {
52 fn is_display_ownership_lost(&self) -> bool {
55 self.signals.contains(*DISPLAY_UNOWNED)
56 }
57
58 fn next_signal(&self) -> Signals {
62 match self.is_display_ownership_lost() {
63 true => *DISPLAY_OWNED,
64 false => *DISPLAY_UNOWNED,
65 }
66 }
67
68 async fn wait_ownership_change<'a, T: AsHandleRef>(
73 &self,
74 event: &'a T,
75 ) -> Result<Signals, Status> {
76 OnSignals::new(event, self.next_signal()).await
77 }
78}
79
80pub struct DisplayOwnership {
100 ownership: Rc<RefCell<Ownership>>,
103
104 key_state: RefCell<KeyState>,
106
107 display_ownership_change_receiver: RefCell<Option<UnboundedReceiver<Ownership>>>,
109
110 _display_ownership_task: Task<()>,
113
114 metrics_logger: metrics::MetricsLogger,
116
117 inspect_status: InputHandlerStatus,
119
120 #[cfg(test)]
126 loop_done: RefCell<Option<UnboundedSender<()>>>,
127}
128
129impl DisplayOwnership {
130 pub fn new(
137 display_ownership_event: impl AsHandleRef + 'static,
138 input_handlers_node: &fuchsia_inspect::Node,
139 metrics_logger: metrics::MetricsLogger,
140 ) -> Rc<Self> {
141 DisplayOwnership::new_internal(
142 display_ownership_event,
143 None,
144 input_handlers_node,
145 metrics_logger,
146 )
147 }
148
149 #[cfg(test)]
150 pub fn new_for_test(
151 display_ownership_event: impl AsHandleRef + 'static,
152 loop_done: UnboundedSender<()>,
153 metrics_logger: metrics::MetricsLogger,
154 ) -> Rc<Self> {
155 let inspector = fuchsia_inspect::Inspector::default();
156 let fake_handlers_node = inspector.root().create_child("input_handlers_node");
157 DisplayOwnership::new_internal(
158 display_ownership_event,
159 Some(loop_done),
160 &fake_handlers_node,
161 metrics_logger,
162 )
163 }
164
165 fn new_internal(
166 display_ownership_event: impl AsHandleRef + 'static,
167 _loop_done: Option<UnboundedSender<()>>,
168 input_handlers_node: &fuchsia_inspect::Node,
169 metrics_logger: metrics::MetricsLogger,
170 ) -> Rc<Self> {
171 let initial_state = display_ownership_event
172 .as_handle_ref()
175 .wait_one(*ANY_DISPLAY_EVENT, MonotonicInstant::INFINITE_PAST)
176 .expect("unable to set the initial display state");
177 log::debug!("setting initial display ownership to: {:?}", &initial_state);
178 let initial_ownership: Ownership = initial_state.into();
179 let ownership = Rc::new(RefCell::new(initial_ownership.clone()));
180
181 let mut ownership_clone = initial_ownership;
182 let (ownership_sender, ownership_receiver) = mpsc::unbounded();
183 let display_ownership_task = Task::local(async move {
184 loop {
185 let signals = ownership_clone.wait_ownership_change(&display_ownership_event).await;
186 match signals {
187 Err(e) => {
188 log::warn!("could not read display state: {:?}", e);
189 break;
190 }
191 Ok(signals) => {
192 log::debug!("setting display ownership to: {:?}", &signals);
193 ownership_sender.unbounded_send(signals.into()).unwrap();
194 ownership_clone = signals.into();
195 }
196 }
197 }
198 log::warn!(
199 "display loop exiting and will no longer monitor display changes - this is not expected"
200 );
201 });
202 log::info!("Display ownership handler installed");
203 let inspect_status = InputHandlerStatus::new(
204 input_handlers_node,
205 "display_ownership",
206 false,
207 );
208 Rc::new(Self {
209 ownership,
210 key_state: RefCell::new(KeyState::new()),
211 display_ownership_change_receiver: RefCell::new(Some(ownership_receiver)),
212 _display_ownership_task: display_ownership_task,
213 metrics_logger,
214 inspect_status,
215 #[cfg(test)]
216 loop_done: RefCell::new(_loop_done),
217 })
218 }
219
220 fn is_display_ownership_lost(&self) -> bool {
222 self.ownership.borrow().is_display_ownership_lost()
223 }
224
225 pub async fn handle_ownership_change(
232 self: &Rc<Self>,
233 output: UnboundedSender<Vec<InputEvent>>,
234 ) -> Result<()> {
235 let mut ownership_source = self
236 .display_ownership_change_receiver
237 .borrow_mut()
238 .take()
239 .context("display_ownership_change_receiver already taken")?;
240 while let Some(new_ownership) = ownership_source.next().await {
241 let is_display_ownership_lost = new_ownership.is_display_ownership_lost();
242 let event_type = match is_display_ownership_lost {
246 true => KeyEventType::Cancel,
247 false => KeyEventType::Sync,
248 };
249 let keys = self.key_state.borrow().get_set();
250 let mut event_time = MonotonicInstant::get();
251 for key in keys.into_iter() {
252 let key_event = KeyboardEvent::new(key, event_type);
253 output
254 .unbounded_send(vec![into_input_event(key_event, event_time)])
255 .context("unable to send display updates")?;
256 event_time = event_time + MonotonicDuration::from_nanos(1);
257 }
258 *(self.ownership.borrow_mut()) = new_ownership;
259 #[cfg(test)]
260 {
261 if let Some(loop_done) = self.loop_done.borrow().as_ref() {
262 loop_done.unbounded_send(()).unwrap();
263 }
264 }
265 }
266 Ok(())
267 }
268}
269
270impl Handler for DisplayOwnership {
271 fn set_handler_healthy(self: std::rc::Rc<Self>) {
272 self.inspect_status.health_node.borrow_mut().set_ok();
273 }
274
275 fn set_handler_unhealthy(self: std::rc::Rc<Self>, msg: &str) {
276 self.inspect_status.health_node.borrow_mut().set_unhealthy(msg);
277 }
278
279 fn get_name(&self) -> &'static str {
280 "DisplayOwnership"
281 }
282
283 fn interest(&self) -> Vec<input_device::InputEventType> {
284 vec![input_device::InputEventType::Keyboard]
285 }
286}
287
288#[async_trait(?Send)]
289impl UnhandledInputHandler for DisplayOwnership {
290 async fn handle_unhandled_input_event(
291 self: Rc<Self>,
292 unhandled_input_event: UnhandledInputEvent,
293 ) -> Vec<input_device::InputEvent> {
294 fuchsia_trace::duration!("input", "display_ownership");
295 self.inspect_status.count_received_event(&unhandled_input_event.event_time);
296 match unhandled_input_event.device_event {
297 input_device::InputDeviceEvent::Keyboard(ref e) => {
298 self.key_state.borrow_mut().update(e.get_event_type(), e.get_key());
299 }
300 _ => {
301 self.metrics_logger.log_error(
302 InputPipelineErrorMetricDimensionEvent::HandlerReceivedUninterestedEvent,
303 std::format!(
304 "{} uninterested input event: {:?}",
305 self.get_name(),
306 unhandled_input_event.get_event_type()
307 ),
308 );
309 }
310 }
311 let is_display_ownership_lost = self.is_display_ownership_lost();
312 if is_display_ownership_lost {
313 self.inspect_status.count_handled_event();
314 }
315
316 #[cfg(test)]
317 {
318 if let Some(loop_done) = self.loop_done.borrow().as_ref() {
319 loop_done.unbounded_send(()).unwrap();
320 }
321 }
322
323 vec![
324 input_device::InputEvent::from(unhandled_input_event)
325 .into_handled_if(is_display_ownership_lost),
326 ]
327 }
328}
329
330fn empty_keyboard_device_descriptor() -> input_device::InputDeviceDescriptor {
331 input_device::InputDeviceDescriptor::Keyboard(
332 KeyboardDeviceDescriptor {
334 keys: vec![],
335 device_information: fidl_fuchsia_input_report::DeviceInformation {
336 vendor_id: Some(0),
337 product_id: Some(0),
338 version: Some(0),
339 polling_rate: Some(0),
340 ..Default::default()
341 },
342 device_id: 0,
343 },
344 )
345}
346
347fn into_input_event(
348 keyboard_event: KeyboardEvent,
349 event_time: MonotonicInstant,
350) -> input_device::InputEvent {
351 input_device::InputEvent {
352 device_event: input_device::InputDeviceEvent::Keyboard(keyboard_event),
353 device_descriptor: empty_keyboard_device_descriptor(),
354 event_time,
355 handled: input_device::Handled::No,
356 trace_id: None,
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use crate::testing_utilities::{create_fake_input_event, create_input_event};
364 use fidl_fuchsia_input::Key;
365 use fuchsia_async as fasync;
366 use pretty_assertions::assert_eq;
367 use std::convert::TryFrom as _;
368 use zx::{EventPair, Peered};
369
370 struct DisplayWrangler {
374 event: EventPair,
375 last: Signals,
376 }
377
378 impl DisplayWrangler {
379 fn new(event: EventPair) -> Self {
380 let mut instance = DisplayWrangler { event, last: *DISPLAY_OWNED };
381 instance.set_unowned();
385 instance
386 }
387
388 fn set_unowned(&mut self) {
389 assert!(self.last != *DISPLAY_UNOWNED, "display is already unowned");
390 self.event.signal_peer(*DISPLAY_OWNED, *DISPLAY_UNOWNED).unwrap();
391 self.last = *DISPLAY_UNOWNED;
392 }
393
394 fn set_owned(&mut self) {
395 assert!(self.last != *DISPLAY_OWNED, "display is already owned");
396 self.event.signal_peer(*DISPLAY_UNOWNED, *DISPLAY_OWNED).unwrap();
397 self.last = *DISPLAY_OWNED;
398 }
399 }
400
401 #[fuchsia::test]
402 async fn display_ownership_change() {
403 let (test_event, handler_event) = EventPair::create();
407
408 let (test_sender, handler_receiver) = mpsc::unbounded::<InputEvent>();
410
411 let (handler_sender, test_receiver) = mpsc::unbounded::<Vec<InputEvent>>();
413
414 let (loop_done_sender, mut loop_done) = mpsc::unbounded::<()>();
417
418 let mut wrangler = DisplayWrangler::new(test_event);
421 let handler = DisplayOwnership::new_for_test(
422 handler_event,
423 loop_done_sender,
424 metrics::MetricsLogger::default(),
425 );
426
427 let handler_clone = handler.clone();
428 let handler_sender_clone = handler_sender.clone();
429 let _task = fasync::Task::local(async move {
430 handler_clone.handle_ownership_change(handler_sender_clone).await.unwrap();
431 });
432
433 let handler_clone_2 = handler.clone();
434 let _input_task = fasync::Task::local(async move {
435 let mut receiver = handler_receiver;
436 while let Some(event) = receiver.next().await {
437 let unhandled_event = UnhandledInputEvent::try_from(event).unwrap();
438 let out_events =
439 handler_clone_2.clone().handle_unhandled_input_event(unhandled_event).await;
440 handler_sender.unbounded_send(out_events).unwrap();
441 }
442 });
443
444 let fake_time = MonotonicInstant::from_nanos(42);
445
446 wrangler.set_owned();
450 loop_done.next().await;
451 test_sender.unbounded_send(create_fake_input_event(fake_time)).unwrap();
452 loop_done.next().await;
453
454 wrangler.set_unowned();
456 loop_done.next().await;
457 test_sender.unbounded_send(create_fake_input_event(fake_time)).unwrap();
458 loop_done.next().await;
459
460 wrangler.set_owned();
462 loop_done.next().await;
463 test_sender.unbounded_send(create_fake_input_event(fake_time)).unwrap();
464 loop_done.next().await;
465
466 wrangler.set_unowned();
468 loop_done.next().await;
469 test_sender.unbounded_send(create_fake_input_event(fake_time)).unwrap();
470 loop_done.next().await;
471
472 let actual: Vec<InputEvent> = test_receiver
473 .take(4)
474 .flat_map(|events| futures::stream::iter(events))
475 .map(|e| e.into_with_event_time(fake_time))
476 .collect()
477 .await;
478
479 assert_eq!(
480 actual,
481 vec![
482 create_fake_input_event(fake_time),
484 create_fake_input_event(fake_time).into_handled(),
486 create_fake_input_event(fake_time),
488 create_fake_input_event(fake_time).into_handled(),
490 ]
491 );
492 }
493
494 fn new_keyboard_input_event(key: Key, event_type: KeyEventType) -> InputEvent {
495 let fake_time = MonotonicInstant::from_nanos(42);
496 create_input_event(
497 KeyboardEvent::new(key, event_type),
498 &input_device::InputDeviceDescriptor::Fake,
499 fake_time,
500 input_device::Handled::No,
501 )
502 }
503
504 #[fuchsia::test]
505 async fn basic_key_state_handling() {
506 let (test_event, handler_event) = EventPair::create();
507 let (test_sender, handler_receiver) = mpsc::unbounded::<InputEvent>();
508 let (handler_sender, test_receiver) = mpsc::unbounded::<Vec<InputEvent>>();
509 let (loop_done_sender, mut loop_done) = mpsc::unbounded::<()>();
510 let mut wrangler = DisplayWrangler::new(test_event);
511 let handler = DisplayOwnership::new_for_test(
512 handler_event,
513 loop_done_sender,
514 metrics::MetricsLogger::default(),
515 );
516
517 let handler_clone = handler.clone();
518 let handler_sender_clone = handler_sender.clone();
519 let _task = fasync::Task::local(async move {
520 handler_clone.handle_ownership_change(handler_sender_clone).await.unwrap();
521 });
522
523 let handler_clone_2 = handler.clone();
524 let _input_task = fasync::Task::local(async move {
525 let mut receiver = handler_receiver;
526 while let Some(event) = receiver.next().await {
527 let unhandled_event = UnhandledInputEvent::try_from(event).unwrap();
528 let out_events =
529 handler_clone_2.clone().handle_unhandled_input_event(unhandled_event).await;
530 handler_sender.unbounded_send(out_events).unwrap();
531 }
532 });
533
534 let fake_time = MonotonicInstant::from_nanos(42);
535
536 wrangler.set_owned();
538 loop_done.next().await;
539 test_sender
540 .unbounded_send(new_keyboard_input_event(Key::A, KeyEventType::Pressed))
541 .unwrap();
542 loop_done.next().await;
543
544 wrangler.set_unowned();
546 loop_done.next().await;
547
548 wrangler.set_owned();
550 loop_done.next().await;
551
552 test_sender
554 .unbounded_send(new_keyboard_input_event(Key::A, KeyEventType::Released))
555 .unwrap();
556 loop_done.next().await;
557
558 let actual: Vec<InputEvent> = test_receiver
559 .take(4)
560 .flat_map(|events| futures::stream::iter(events))
561 .map(|e| e.into_with_event_time(fake_time))
562 .collect()
563 .await;
564
565 assert_eq!(
566 actual,
567 vec![
568 new_keyboard_input_event(Key::A, KeyEventType::Pressed),
569 new_keyboard_input_event(Key::A, KeyEventType::Cancel)
570 .into_with_device_descriptor(empty_keyboard_device_descriptor()),
571 new_keyboard_input_event(Key::A, KeyEventType::Sync)
572 .into_with_device_descriptor(empty_keyboard_device_descriptor()),
573 new_keyboard_input_event(Key::A, KeyEventType::Released),
574 ]
575 );
576 }
577
578 #[fuchsia::test]
579 async fn more_key_state_handling() {
580 let (test_event, handler_event) = EventPair::create();
581 let (test_sender, handler_receiver) = mpsc::unbounded::<InputEvent>();
582 let (handler_sender, test_receiver) = mpsc::unbounded::<Vec<InputEvent>>();
583 let (loop_done_sender, mut loop_done) = mpsc::unbounded::<()>();
584 let mut wrangler = DisplayWrangler::new(test_event);
585 let handler = DisplayOwnership::new_for_test(
586 handler_event,
587 loop_done_sender,
588 metrics::MetricsLogger::default(),
589 );
590
591 let handler_clone = handler.clone();
592 let handler_sender_clone = handler_sender.clone();
593 let _task = fasync::Task::local(async move {
594 handler_clone.handle_ownership_change(handler_sender_clone).await.unwrap();
595 });
596
597 let handler_clone_2 = handler.clone();
598 let _input_task = fasync::Task::local(async move {
599 let mut receiver = handler_receiver;
600 while let Some(event) = receiver.next().await {
601 let unhandled_event = UnhandledInputEvent::try_from(event).unwrap();
602 let out_events =
603 handler_clone_2.clone().handle_unhandled_input_event(unhandled_event).await;
604 handler_sender.unbounded_send(out_events).unwrap();
605 }
606 });
607
608 let fake_time = MonotonicInstant::from_nanos(42);
609
610 wrangler.set_owned();
611 loop_done.next().await;
612 test_sender
613 .unbounded_send(new_keyboard_input_event(Key::A, KeyEventType::Pressed))
614 .unwrap();
615 loop_done.next().await;
616 test_sender
617 .unbounded_send(new_keyboard_input_event(Key::B, KeyEventType::Pressed))
618 .unwrap();
619 loop_done.next().await;
620
621 wrangler.set_unowned();
623 loop_done.next().await;
624 test_sender
625 .unbounded_send(new_keyboard_input_event(Key::B, KeyEventType::Released))
626 .unwrap();
627 loop_done.next().await;
628 test_sender
629 .unbounded_send(new_keyboard_input_event(Key::C, KeyEventType::Pressed))
630 .unwrap();
631 loop_done.next().await;
632
633 wrangler.set_owned();
635 loop_done.next().await;
636
637 test_sender
639 .unbounded_send(new_keyboard_input_event(Key::A, KeyEventType::Released))
640 .unwrap();
641 loop_done.next().await;
642 test_sender
643 .unbounded_send(new_keyboard_input_event(Key::C, KeyEventType::Released))
644 .unwrap();
645 loop_done.next().await;
646
647 let actual: Vec<InputEvent> = test_receiver
648 .take(10) .flat_map(|events| futures::stream::iter(events))
650 .map(|e| e.into_with_event_time(fake_time))
651 .collect()
652 .await;
653
654 assert_eq!(
655 actual,
656 vec![
657 new_keyboard_input_event(Key::A, KeyEventType::Pressed),
658 new_keyboard_input_event(Key::B, KeyEventType::Pressed),
659 new_keyboard_input_event(Key::A, KeyEventType::Cancel)
660 .into_with_device_descriptor(empty_keyboard_device_descriptor()),
661 new_keyboard_input_event(Key::B, KeyEventType::Cancel)
662 .into_with_device_descriptor(empty_keyboard_device_descriptor()),
663 new_keyboard_input_event(Key::B, KeyEventType::Released).into_handled(),
664 new_keyboard_input_event(Key::C, KeyEventType::Pressed).into_handled(),
665 new_keyboard_input_event(Key::A, KeyEventType::Sync)
669 .into_with_device_descriptor(empty_keyboard_device_descriptor()),
670 new_keyboard_input_event(Key::C, KeyEventType::Sync)
671 .into_with_device_descriptor(empty_keyboard_device_descriptor()),
672 new_keyboard_input_event(Key::A, KeyEventType::Released),
673 new_keyboard_input_event(Key::C, KeyEventType::Released),
674 ]
675 );
676 }
677
678 #[fuchsia::test]
679 async fn display_ownership_initialized_with_inspect_node() {
680 let (test_event, handler_event) = EventPair::create();
681 let (loop_done_sender, _) = mpsc::unbounded::<()>();
682 let inspector = fuchsia_inspect::Inspector::default();
683 let fake_handlers_node = inspector.root().create_child("input_handlers_node");
684 let _ = DisplayWrangler::new(test_event);
686 let _handler = DisplayOwnership::new_internal(
687 handler_event,
688 Some(loop_done_sender),
689 &fake_handlers_node,
690 metrics::MetricsLogger::default(),
691 );
692 diagnostics_assertions::assert_data_tree!(inspector, root: {
693 input_handlers_node: {
694 display_ownership: {
695 events_received_count: 0u64,
696 events_handled_count: 0u64,
697 last_received_timestamp_ns: 0u64,
698 "fuchsia.inspect.Health": {
699 status: "STARTING_UP",
700 start_timestamp_nanos: diagnostics_assertions::AnyProperty
703 },
704 }
705 }
706 });
707 }
708
709 #[fuchsia::test]
710 async fn display_ownership_inspect_counts_events() {
711 let (test_event, handler_event) = EventPair::create();
712 let (test_sender, handler_receiver) = mpsc::unbounded::<InputEvent>();
713 let (handler_sender, _test_receiver) = mpsc::unbounded::<Vec<InputEvent>>();
714 let (loop_done_sender, mut loop_done) = mpsc::unbounded::<()>();
715 let mut wrangler = DisplayWrangler::new(test_event);
716 let inspector = fuchsia_inspect::Inspector::default();
717 let fake_handlers_node = inspector.root().create_child("input_handlers_node");
718 let handler = DisplayOwnership::new_internal(
719 handler_event,
720 Some(loop_done_sender),
721 &fake_handlers_node,
722 metrics::MetricsLogger::default(),
723 );
724 let handler_clone = handler.clone();
725 let handler_sender_clone = handler_sender.clone();
726 let _task = fasync::Task::local(async move {
727 handler_clone.handle_ownership_change(handler_sender_clone).await.unwrap();
728 });
729
730 let handler_clone_2 = handler.clone();
731 let _input_task = fasync::Task::local(async move {
732 let mut receiver = handler_receiver;
733 while let Some(event) = receiver.next().await {
734 let unhandled_event = UnhandledInputEvent::try_from(event).unwrap();
735 let out_events =
736 handler_clone_2.clone().handle_unhandled_input_event(unhandled_event).await;
737 handler_sender.unbounded_send(out_events).unwrap();
738 }
739 });
740
741 wrangler.set_owned();
743 loop_done.next().await;
744 test_sender
745 .unbounded_send(new_keyboard_input_event(Key::A, KeyEventType::Pressed))
746 .unwrap();
747 loop_done.next().await;
748
749 wrangler.set_unowned();
752 loop_done.next().await;
753 test_sender
754 .unbounded_send(new_keyboard_input_event(Key::B, KeyEventType::Pressed))
755 .unwrap();
756 loop_done.next().await;
757
758 wrangler.set_owned();
760 loop_done.next().await;
761
762 test_sender
764 .unbounded_send(new_keyboard_input_event(Key::A, KeyEventType::Released))
765 .unwrap();
766 loop_done.next().await;
767
768 diagnostics_assertions::assert_data_tree!(inspector, root: {
769 input_handlers_node: {
770 display_ownership: {
771 events_received_count: 3u64,
772 events_handled_count: 1u64,
773 last_received_timestamp_ns: 42u64,
774 "fuchsia.inspect.Health": {
775 status: "STARTING_UP",
776 start_timestamp_nanos: diagnostics_assertions::AnyProperty
779 },
780 }
781 }
782 });
783 }
784}