pythonnative 0.25.0__py3-none-any.whl → 0.27.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. pythonnative/__init__.py +1 -1
  2. pythonnative/animated.py +697 -62
  3. pythonnative/cli/pn.py +172 -12
  4. pythonnative/components.py +61 -7
  5. pythonnative/diagnostics.py +27 -0
  6. pythonnative/gestures.py +446 -34
  7. pythonnative/native_modules/app_state.py +2 -1
  8. pythonnative/native_modules/battery.py +3 -2
  9. pythonnative/native_modules/biometrics.py +2 -1
  10. pythonnative/native_modules/camera.py +16 -7
  11. pythonnative/native_modules/clipboard.py +3 -2
  12. pythonnative/native_modules/file_system.py +3 -2
  13. pythonnative/native_modules/haptics.py +7 -6
  14. pythonnative/native_modules/linking.py +62 -6
  15. pythonnative/native_modules/location.py +11 -8
  16. pythonnative/native_modules/net_info.py +75 -5
  17. pythonnative/native_modules/notifications.py +134 -15
  18. pythonnative/native_modules/permissions.py +3 -2
  19. pythonnative/native_modules/share.py +2 -1
  20. pythonnative/native_views/android.py +461 -137
  21. pythonnative/native_views/desktop.py +816 -96
  22. pythonnative/native_views/ios.py +750 -193
  23. pythonnative/project/android.py +15 -3
  24. pythonnative/project/builder.py +172 -70
  25. pythonnative/project/config.py +28 -6
  26. pythonnative/project/devices.py +291 -0
  27. pythonnative/project/doctor.py +5 -4
  28. pythonnative/project/ios.py +51 -77
  29. pythonnative/project/permissions.py +19 -1
  30. pythonnative/project/runtime_assets.py +78 -166
  31. pythonnative/reconciler.py +28 -0
  32. pythonnative/style.py +19 -0
  33. pythonnative/templates/android_template/app/build.gradle +9 -0
  34. pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +5 -0
  35. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +148 -12
  36. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNFrameLayout.kt +50 -0
  37. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +41 -17
  38. pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
  39. pythonnative/templates/ios_template/ios_template/Info.plist +5 -2
  40. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +397 -0
  41. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +31 -20
  42. pythonnative/templates/ios_template/ios_template/ViewController.swift +77 -198
  43. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +72 -47
  44. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/METADATA +3 -3
  45. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/RECORD +49 -46
  46. pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -24
  47. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/WHEEL +0 -0
  48. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/entry_points.txt +0 -0
  49. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/licenses/LICENSE +0 -0
  50. {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/top_level.txt +0 -0
@@ -21,12 +21,28 @@ applies those frames via per-child `MarginLayoutParams`.
21
21
 
22
22
  **Gestures** feed raw ``MotionEvent`` streams into the shared
23
23
  pure-Python [`GestureArbiter`][pythonnative.gestures.GestureArbiter],
24
- so gesture semantics match the desktop preview exactly.
24
+ so gesture semantics match the desktop preview exactly. Serialized
25
+ specs (including ``simultaneous`` / ``wait_for`` composition metadata
26
+ and ``fling`` configs) pass through untouched; this module only
27
+ schedules main-looper polls at the arbiter's ``next_deadline()`` so
28
+ long-press activation and multi-tap windows fire even when no
29
+ further touch events arrive.
25
30
 
26
31
  **Animations**: ``timing`` animations on transform/opacity/color props
27
32
  are driven natively by ``ObjectAnimator`` (Choreographer-paced, no
28
33
  Python per-frame work); springs and decay fall back to the Python
29
- ticker.
34
+ ticker, whose per-frame values arrive through
35
+ ``set_animated_property`` (floats for transforms and opacity,
36
+ interpolated "#AARRGGBB" strings for background and text colors that
37
+ re-bake through the border-drawable machinery instead of clobbering
38
+ it).
39
+
40
+ **Interaction props**: ``pointer_events`` maps onto the template's
41
+ ``PNFrameLayout`` touch interception when available (with a
42
+ Python-side fallback for older projects), ``hit_slop`` expands touch
43
+ targets via a parent ``TouchDelegate`` sized after layout,
44
+ ``z_index`` maps to ``View.setZ``, and per-corner radii extend the
45
+ ``GradientDrawable`` border path.
30
46
 
31
47
  This module is only imported on Android at runtime. Desktop tests
32
48
  inject a mock registry via
@@ -40,6 +56,7 @@ from typing import Any, Callable, Dict, Optional, Tuple
40
56
 
41
57
  from java import dynamic_proxy, jclass
42
58
 
59
+ from .. import diagnostics
43
60
  from ..events import dispatch_event, event_names
44
61
  from ..gestures import make_arbiter
45
62
  from ..utils import get_android_context
@@ -57,11 +74,20 @@ _SIDE_BORDER_COLOR_KEYS = (
57
74
  "border_right_color",
58
75
  "border_bottom_color",
59
76
  )
77
+ # Order matches GradientDrawable.setCornerRadii: top-left, top-right,
78
+ # bottom-right, bottom-left (each corner takes an x and a y radius).
79
+ _CORNER_RADIUS_KEYS = (
80
+ "border_top_left_radius",
81
+ "border_top_right_radius",
82
+ "border_bottom_right_radius",
83
+ "border_bottom_left_radius",
84
+ )
60
85
  _DRAWABLE_STYLE_KEYS = (
61
86
  "background_color",
62
87
  "border_radius",
63
88
  "border_width",
64
89
  "border_color",
90
+ *_CORNER_RADIUS_KEYS,
65
91
  *_SIDE_BORDER_WIDTH_KEYS,
66
92
  *_SIDE_BORDER_COLOR_KEYS,
67
93
  )
@@ -97,6 +123,38 @@ def _pn_runtime_class(class_name: str) -> Any:
97
123
  return jclass(f"{package}.{class_name}")
98
124
 
99
125
 
126
+ # Cached lookup for the template's PNFrameLayout container class.
127
+ # ``None`` means "not resolved yet"; ``False`` means "this generated
128
+ # project predates the class", so creation falls back to FrameLayout
129
+ # without retrying the (exception-throwing) lookup per view.
130
+ _pn_container_cls: Any = None
131
+
132
+
133
+ def _new_container() -> Any:
134
+ """Create a container view, preferring the template's ``PNFrameLayout``.
135
+
136
+ ``PNFrameLayout`` adds native ``pointer_events`` interception
137
+ (``onInterceptTouchEvent`` / ``dispatchTouchEvent``), which
138
+ Chaquopy can't express from Python. Older generated projects
139
+ without the class get a plain ``FrameLayout``; the
140
+ ``pointer_events`` prop then degrades to the Python-side
141
+ approximation in
142
+ [`_apply_pointer_events`][pythonnative.native_views.android._apply_pointer_events].
143
+ """
144
+ global _pn_container_cls
145
+ if _pn_container_cls is None:
146
+ try:
147
+ _pn_container_cls = _pn_runtime_class("PNFrameLayout")
148
+ except Exception:
149
+ _pn_container_cls = False
150
+ if _pn_container_cls is not False:
151
+ try:
152
+ return _pn_container_cls(_ctx())
153
+ except Exception:
154
+ diagnostics.swallowed("android._new_container")
155
+ return jclass("android.widget.FrameLayout")(_ctx())
156
+
157
+
100
158
  def _signed_color(value: int) -> int:
101
159
  """Clamp a Python color int into Java's signed 32-bit range."""
102
160
  value &= 0xFFFFFFFF
@@ -206,7 +264,7 @@ def _apply_side_border(view: Any, props: Dict[str, Any]) -> bool:
206
264
  try:
207
265
  view.invalidate()
208
266
  except Exception:
209
- pass
267
+ diagnostics.swallowed("android._apply_side_border")
210
268
  return True
211
269
  except Exception:
212
270
  return False
@@ -226,7 +284,7 @@ def _apply_border(view: Any, props: Dict[str, Any]) -> None:
226
284
  """
227
285
  if _apply_side_border(view, props):
228
286
  return
229
- has_border = any(k in props for k in ("border_radius", "border_width", "border_color"))
287
+ has_border = any(k in props for k in ("border_radius", "border_width", "border_color", *_CORNER_RADIUS_KEYS))
230
288
  has_bg = "background_color" in props and props["background_color"] is not None
231
289
  if not has_border and not has_bg:
232
290
  return
@@ -237,12 +295,26 @@ def _apply_border(view: Any, props: Dict[str, Any]) -> None:
237
295
  try:
238
296
  drawable.setColor(parse_color_int(props["background_color"]))
239
297
  except Exception:
240
- pass
241
- if "border_radius" in props and props["border_radius"] is not None:
298
+ diagnostics.swallowed("android._apply_border")
299
+ if any(props.get(k) is not None for k in _CORNER_RADIUS_KEYS):
300
+ # Per-corner radii: unspecified corners inherit the uniform
301
+ # ``border_radius`` (else 0). ``setCornerRadii`` takes eight
302
+ # floats, an (x, y) pair per corner in _CORNER_RADIUS_KEYS
303
+ # order, each in pixels.
304
+ try:
305
+ base_radius = float(props.get("border_radius") or 0.0)
306
+ radii = []
307
+ for key in _CORNER_RADIUS_KEYS:
308
+ r = float(_dp(float(props[key] if props.get(key) is not None else base_radius)))
309
+ radii.extend([r, r])
310
+ drawable.setCornerRadii(radii)
311
+ except Exception:
312
+ diagnostics.swallowed("android._apply_border")
313
+ elif "border_radius" in props and props["border_radius"] is not None:
242
314
  try:
243
315
  drawable.setCornerRadius(float(_dp(float(props["border_radius"]))))
244
316
  except Exception:
245
- pass
317
+ diagnostics.swallowed("android._apply_border")
246
318
  if ("border_width" in props and props["border_width"] is not None) or (
247
319
  "border_color" in props and props["border_color"] is not None
248
320
  ):
@@ -254,18 +326,56 @@ def _apply_border(view: Any, props: Dict[str, Any]) -> None:
254
326
  parse_color_int(color or "#000000"),
255
327
  )
256
328
  except Exception:
257
- pass
329
+ diagnostics.swallowed("android._apply_border")
258
330
  view.setBackground(drawable)
259
331
  try:
260
332
  drawable.invalidateSelf()
261
333
  except Exception:
262
- pass
334
+ diagnostics.swallowed("android._apply_border")
263
335
  try:
264
336
  view.invalidate()
265
337
  except Exception:
266
- pass
338
+ diagnostics.swallowed("android._apply_border")
267
339
  except Exception:
268
- pass
340
+ diagnostics.swallowed("android._apply_border")
341
+
342
+
343
+ def _set_animated_background(view: Any, value: Any) -> None:
344
+ """Apply one animated ``background_color`` frame without losing borders.
345
+
346
+ Interpolations hand this module raw color values ("#AARRGGBB" or
347
+ "#RRGGBB" strings, or ints). Views whose background is a
348
+ border-rendering drawable (the uniform ``GradientDrawable`` or the
349
+ per-side ``PNBorderDrawable``) must keep their corner radii and
350
+ strokes while the fill animates, so the color routes through the
351
+ same [`_apply_border`][pythonnative.native_views.android._apply_border]
352
+ machinery ``update()`` uses. When the current background already is
353
+ a ``GradientDrawable``, its fill is mutated in place, so no
354
+ drawable is allocated per animation frame. Plain views take the
355
+ direct ``setBackgroundColor`` path.
356
+ """
357
+ state = _state_of(view)
358
+ visual = state.get("visual")
359
+ has_border = isinstance(visual, dict) and any(
360
+ visual.get(k) is not None for k in _DRAWABLE_STYLE_KEYS if k != "background_color"
361
+ )
362
+ if has_border:
363
+ visual = dict(visual)
364
+ visual["background_color"] = value
365
+ state["visual"] = visual
366
+ try:
367
+ GradientDrawable = jclass("android.graphics.drawable.GradientDrawable")
368
+ bg = view.getBackground()
369
+ if bg is not None and isinstance(bg, GradientDrawable):
370
+ bg.setColor(parse_color_int(value))
371
+ return
372
+ except Exception:
373
+ diagnostics.swallowed("android._set_animated_background")
374
+ # Per-side ``PNBorderDrawable`` backgrounds (immutable fill) and
375
+ # first frames before any drawable exists re-bake the full set.
376
+ _apply_border(view, visual)
377
+ return
378
+ view.setBackgroundColor(parse_color_int(value))
269
379
 
270
380
 
271
381
  def _apply_shadow(view: Any, props: Dict[str, Any]) -> None:
@@ -291,7 +401,7 @@ def _apply_shadow(view: Any, props: Dict[str, Any]) -> None:
291
401
  try:
292
402
  view.setElevation(float(_dp(float(elevation))))
293
403
  except Exception:
294
- pass
404
+ diagnostics.swallowed("android._apply_shadow")
295
405
  color = props.get("shadow_color")
296
406
  if color is None:
297
407
  return
@@ -308,7 +418,7 @@ def _apply_shadow(view: Any, props: Dict[str, Any]) -> None:
308
418
  view.setOutlineAmbientShadowColor(signed)
309
419
  view.setOutlineSpotShadowColor(signed)
310
420
  except Exception:
311
- pass
421
+ diagnostics.swallowed("android._apply_shadow")
312
422
 
313
423
 
314
424
  def _apply_transform(view: Any, props: Dict[str, Any]) -> None:
@@ -324,7 +434,7 @@ def _apply_transform(view: Any, props: Dict[str, Any]) -> None:
324
434
  view.setTranslationX(0.0)
325
435
  view.setTranslationY(0.0)
326
436
  except Exception:
327
- pass
437
+ diagnostics.swallowed("android._apply_transform")
328
438
  return
329
439
  entries = spec if isinstance(spec, list) else [spec]
330
440
  for entry in entries:
@@ -353,7 +463,7 @@ def _apply_transform(view: Any, props: Dict[str, Any]) -> None:
353
463
  if "translate_y" in entry:
354
464
  view.setTranslationY(float(_dp(float(entry["translate_y"]))))
355
465
  except Exception:
356
- pass
466
+ diagnostics.swallowed("android._apply_transform")
357
467
 
358
468
 
359
469
  def _apply_accessibility(view: Any, props: Dict[str, Any]) -> None:
@@ -365,13 +475,13 @@ def _apply_accessibility(view: Any, props: Dict[str, Any]) -> None:
365
475
  View.IMPORTANT_FOR_ACCESSIBILITY_YES if props["accessible"] else View.IMPORTANT_FOR_ACCESSIBILITY_NO
366
476
  )
367
477
  except Exception:
368
- pass
478
+ diagnostics.swallowed("android._apply_accessibility")
369
479
  if "accessibility_label" in props:
370
480
  try:
371
481
  label = props["accessibility_label"]
372
482
  view.setContentDescription(str(label) if label is not None else None)
373
483
  except Exception:
374
- pass
484
+ diagnostics.swallowed("android._apply_accessibility")
375
485
  if "accessibility_live_region" in props:
376
486
  try:
377
487
  mode = {"none": 0, "polite": 1, "assertive": 2}.get(
@@ -379,14 +489,14 @@ def _apply_accessibility(view: Any, props: Dict[str, Any]) -> None:
379
489
  )
380
490
  view.setAccessibilityLiveRegion(mode)
381
491
  except Exception:
382
- pass
492
+ diagnostics.swallowed("android._apply_accessibility")
383
493
 
384
494
  state = props.get("accessibility_state")
385
495
  if isinstance(state, dict) and "selected" in state:
386
496
  try:
387
497
  view.setSelected(bool(state["selected"]))
388
498
  except Exception:
389
- pass
499
+ diagnostics.swallowed("android._apply_accessibility")
390
500
  test_id = props.get("test_id")
391
501
  if test_id is None and not isinstance(state, dict):
392
502
  return
@@ -413,7 +523,139 @@ def _apply_accessibility(view: Any, props: Dict[str, Any]) -> None:
413
523
  delegate.setStateBusy(state.get("busy"))
414
524
  delegate.setStateExpanded(state.get("expanded"))
415
525
  except Exception:
526
+ diagnostics.swallowed("android._apply_accessibility")
527
+
528
+
529
+ def _pointer_events_blocked(view: Any) -> bool:
530
+ """Whether ``pointer_events`` disables the view's own touch handling.
531
+
532
+ Consulted at the top of every touch-stream proxy (gesture, press)
533
+ so listeners installed at create time honor the prop without being
534
+ re-wired: ``"none"`` and ``"box_none"`` both mean the view itself
535
+ must not observe or consume the stream.
536
+ """
537
+ merged = _state_of(view).get("props") or {}
538
+ return merged.get("pointer_events") in ("none", "box_none")
539
+
540
+
541
+ def _apply_pointer_events(view: Any, value: Any) -> None:
542
+ """Apply the ``pointer_events`` prop (``None`` restores ``"auto"``).
543
+
544
+ Two cooperating layers implement the modes:
545
+
546
+ - **Native interception** (preferred): containers built by
547
+ [`_new_container`][pythonnative.native_views.android._new_container]
548
+ are the template's ``PNFrameLayout``, whose
549
+ ``setPointerEventsMode(String)`` drives real
550
+ ``dispatchTouchEvent`` / ``onInterceptTouchEvent`` overrides.
551
+ ``"none"`` declines the whole dispatch so touches pass through
552
+ to views underneath, ``"box_only"`` intercepts events away from
553
+ children, and ``"box_none"`` never intercepts. The call is
554
+ guarded so older generated projects without the class (and
555
+ non-container views) fall back cleanly.
556
+ - **Python-side muting**: ``setClickable`` toggles whether the
557
+ view claims events, and the touch-stream proxies consult
558
+ [`_pointer_events_blocked`][pythonnative.native_views.android._pointer_events_blocked]
559
+ so gesture/press wiring goes quiet without being torn down.
560
+ ``"auto"`` / removal restores the original clickability
561
+ captured before the first override.
562
+
563
+ On a plain ``FrameLayout`` (old template) the fallback keeps the
564
+ previous approximation: ``"none"`` silences the view itself but
565
+ interactive children keep receiving touches, and ``"box_only"``
566
+ only claims events no interactive child consumed. ``"box_none"``
567
+ is exact either way, since children need no help.
568
+ """
569
+ mode = value if value is not None else "auto"
570
+ state = _state_of(view)
571
+ try:
572
+ view.setPointerEventsMode(str(mode))
573
+ except Exception:
574
+ # Plain FrameLayout, a leaf widget, or an older template
575
+ # without PNFrameLayout; the Python-side fallback still runs.
416
576
  pass
577
+ try:
578
+ if mode in ("none", "box_none"):
579
+ state.setdefault("pe_was_clickable", bool(view.isClickable()))
580
+ view.setClickable(False)
581
+ elif mode == "box_only":
582
+ state.setdefault("pe_was_clickable", bool(view.isClickable()))
583
+ view.setClickable(True)
584
+ elif "pe_was_clickable" in state:
585
+ view.setClickable(bool(state.pop("pe_was_clickable")))
586
+ except Exception:
587
+ diagnostics.swallowed("android._apply_pointer_events")
588
+
589
+
590
+ def _hit_slop_insets(value: Any) -> Optional[Tuple[float, float, float, float]]:
591
+ """Normalize a ``hit_slop`` prop into ``(top, left, bottom, right)`` points.
592
+
593
+ Accepts a uniform number or a dict with any of ``top`` / ``left``
594
+ / ``bottom`` / ``right``. Returns ``None`` for absent or
595
+ unparseable values.
596
+ """
597
+ if value is None:
598
+ return None
599
+ if isinstance(value, dict):
600
+ try:
601
+ return (
602
+ float(value.get("top") or 0.0),
603
+ float(value.get("left") or 0.0),
604
+ float(value.get("bottom") or 0.0),
605
+ float(value.get("right") or 0.0),
606
+ )
607
+ except Exception:
608
+ return None
609
+ try:
610
+ uniform = float(value)
611
+ except Exception:
612
+ return None
613
+ return (uniform, uniform, uniform, uniform)
614
+
615
+
616
+ def _update_hit_slop(view: Any) -> None:
617
+ """Extend the view's touch target via a ``TouchDelegate`` on its parent.
618
+
619
+ The standard Android mechanism: the parent forwards touches
620
+ landing inside an expanded ``Rect`` (the view's frame plus the
621
+ slop insets, in parent-relative pixels) to the view. Parents are
622
+ PythonNative-managed ``FrameLayout``s, so installing the delegate
623
+ is safe; a parent only holds one delegate, so among siblings the
624
+ last ``hit_slop`` applied wins (matching the platform limitation).
625
+
626
+ Requires a known frame, so
627
+ [`set_frame`][pythonnative.native_views.android.AndroidViewHandler.set_frame]
628
+ records the frame and re-invokes this; prop-only updates before
629
+ the first frame defer until layout runs.
630
+ """
631
+ state = _state_of(view)
632
+ frame = state.get("frame")
633
+ if frame is None:
634
+ return
635
+ merged = state.get("props") or {}
636
+ insets = _hit_slop_insets(merged.get("hit_slop"))
637
+ try:
638
+ parent = view.getParent()
639
+ if parent is None:
640
+ return
641
+ if insets is None:
642
+ if state.pop("hit_slop_active", None):
643
+ parent.setTouchDelegate(None)
644
+ return
645
+ top, left, bottom, right = insets
646
+ x, y, width, height = frame
647
+ Rect = jclass("android.graphics.Rect")
648
+ TouchDelegate = jclass("android.view.TouchDelegate")
649
+ rect = Rect(
650
+ _dp(x - left),
651
+ _dp(y - top),
652
+ _dp(x + width + right),
653
+ _dp(y + height + bottom),
654
+ )
655
+ parent.setTouchDelegate(TouchDelegate(rect, view))
656
+ state["hit_slop_active"] = True
657
+ except Exception:
658
+ diagnostics.swallowed("android._update_hit_slop")
417
659
 
418
660
 
419
661
  def _apply_common_visual(view: Any, props: Dict[str, Any]) -> None:
@@ -433,12 +675,24 @@ def _apply_common_visual(view: Any, props: Dict[str, Any]) -> None:
433
675
  view.setClipChildren(clip)
434
676
  view.setClipToPadding(clip)
435
677
  except Exception:
436
- pass
678
+ diagnostics.swallowed("android._apply_common_visual")
437
679
  if "opacity" in props and props["opacity"] is not None:
438
680
  try:
439
681
  view.setAlpha(float(props["opacity"]))
440
682
  except Exception:
441
- pass
683
+ diagnostics.swallowed("android._apply_common_visual")
684
+ if "z_index" in props:
685
+ # ``setZ`` takes pixels, so scale like other coordinates; a
686
+ # removal (``None``) resets the view to the default plane.
687
+ try:
688
+ z = props["z_index"]
689
+ view.setZ(float(_dp(float(z))) if z is not None else 0.0)
690
+ except Exception:
691
+ diagnostics.swallowed("android._apply_common_visual")
692
+ if "pointer_events" in props:
693
+ _apply_pointer_events(view, props["pointer_events"])
694
+ if "hit_slop" in props:
695
+ _update_hit_slop(view)
442
696
  _apply_shadow(view, props)
443
697
  _apply_transform(view, props)
444
698
  _apply_accessibility(view, props)
@@ -453,10 +707,14 @@ def _wire_gestures(view: Any, specs: Any) -> None:
453
707
  """Feed ``MotionEvent`` streams on ``view`` into a `GestureArbiter`.
454
708
 
455
709
  The arbiter emits ``(gesture_index, payload)`` pairs which are
456
- forwarded as ``gesture:<i>`` events for this view's tag. Long
457
- presses are polled with a main-looper ``Handler``. When a pan
458
- activates, the parent is asked not to intercept so an enclosing
459
- ScrollView can't steal the drag.
710
+ forwarded as ``gesture:<i>`` events for this view's tag. Specs
711
+ (including ``simultaneous`` / ``wait_for`` metadata and fling
712
+ configs) pass through to the arbiter untouched; all arbitration
713
+ and recognition live in ``pythonnative.gestures``. Time-based
714
+ deadlines (long press, multi-tap windows) are polled with a
715
+ main-looper ``Handler`` scheduled at the arbiter's
716
+ ``next_deadline()``. When a pan activates, the parent is asked
717
+ not to intercept so an enclosing ScrollView can't steal the drag.
460
718
  """
461
719
  state = _state_of(view)
462
720
  if not isinstance(specs, (list, tuple)) or not specs:
@@ -475,13 +733,27 @@ def _wire_gestures(view: Any, specs: Any) -> None:
475
733
 
476
734
 
477
735
  def _schedule_arbiter_poll(view: Any, state: Dict[str, Any]) -> None:
478
- """Schedule a main-looper poll at the arbiter's next deadline (long-press)."""
736
+ """Schedule a main-looper poll at the arbiter's next deadline.
737
+
738
+ Deadlines cover every time-based recognizer decision: long-press
739
+ activation and the multi-tap windows that flush an Exclusive
740
+ double-tap/single-tap buffer. Called after every pointer event
741
+ (not just DOWN) because a deadline can appear on UP, when no
742
+ further touch events arrive to drive the arbiter forward.
743
+
744
+ A pending poll at or before the requested deadline is reused
745
+ rather than stacking duplicate runnables; each poll reschedules
746
+ itself for the following deadline until none remains.
747
+ """
479
748
  arbiter = state.get("arbiter")
480
749
  if arbiter is None:
481
750
  return
482
751
  deadline = arbiter.next_deadline()
483
752
  if deadline is None:
484
753
  return
754
+ pending = state.get("poll_deadline")
755
+ if pending is not None and pending <= deadline + 0.001:
756
+ return
485
757
  delay_ms = max(1, int((deadline - time.monotonic()) * 1000.0))
486
758
  try:
487
759
  Handler = jclass("android.os.Handler")
@@ -491,9 +763,14 @@ def _schedule_arbiter_poll(view: Any, state: Dict[str, Any]) -> None:
491
763
  if handler is None:
492
764
  handler = Handler(Looper.getMainLooper())
493
765
  state["poll_handler"] = handler
766
+ token = state.get("poll_token", 0) + 1
767
+ state["poll_token"] = token
768
+ state["poll_deadline"] = deadline
494
769
 
495
770
  class _PollRunnable(dynamic_proxy(Runnable)):
496
771
  def run(self) -> None:
772
+ if state.get("poll_token") == token:
773
+ state["poll_deadline"] = None
497
774
  live = state.get("arbiter")
498
775
  if live is not None:
499
776
  live.poll(time.monotonic())
@@ -501,7 +778,8 @@ def _schedule_arbiter_poll(view: Any, state: Dict[str, Any]) -> None:
501
778
 
502
779
  handler.postDelayed(_PollRunnable(), delay_ms)
503
780
  except Exception:
504
- pass
781
+ state["poll_deadline"] = None
782
+ diagnostics.swallowed("android._schedule_arbiter_poll")
505
783
 
506
784
 
507
785
  def _bind_touch_stream(view: Any, state: Dict[str, Any]) -> None:
@@ -511,11 +789,13 @@ def _bind_touch_stream(view: Any, state: Dict[str, Any]) -> None:
511
789
 
512
790
  class _GestureTouchProxy(dynamic_proxy(OnTouchListener)):
513
791
  def onTouch(self, v: Any, event: Any) -> bool:
792
+ if _pointer_events_blocked(v):
793
+ return False
514
794
  return bool(_feed_motion_event(v, state, event))
515
795
 
516
796
  view.setOnTouchListener(_GestureTouchProxy())
517
797
  except Exception:
518
- pass
798
+ diagnostics.swallowed("android._bind_touch_stream")
519
799
 
520
800
 
521
801
  def _feed_motion_event(view: Any, state: Dict[str, Any], event: Any) -> bool:
@@ -535,7 +815,6 @@ def _feed_motion_event(view: Any, state: Dict[str, Any], event: Any) -> bool:
535
815
  idx = int(event.getActionIndex())
536
816
  pid = int(event.getPointerId(idx))
537
817
  arbiter.pointer_down(pid, float(event.getX(idx)) / density, float(event.getY(idx)) / density, t)
538
- _schedule_arbiter_poll(view, state)
539
818
  elif action == 2: # MOVE
540
819
  for i in range(int(event.getPointerCount())):
541
820
  pid = int(event.getPointerId(i))
@@ -546,13 +825,17 @@ def _feed_motion_event(view: Any, state: Dict[str, Any], event: Any) -> bool:
546
825
  if parent is not None:
547
826
  parent.requestDisallowInterceptTouchEvent(True)
548
827
  except Exception:
549
- pass
828
+ diagnostics.swallowed("android._feed_motion_event")
550
829
  elif action in (1, 6): # UP / POINTER_UP
551
830
  idx = int(event.getActionIndex())
552
831
  pid = int(event.getPointerId(idx))
553
832
  arbiter.pointer_up(pid, float(event.getX(idx)) / density, float(event.getY(idx)) / density, t)
554
833
  elif action == 3: # CANCEL
555
834
  arbiter.cancel(t)
835
+ # Every event can move the next time-based deadline (long-press
836
+ # activation, multi-tap windows opened on UP), so reschedule
837
+ # unconditionally; the scheduler dedups pending polls.
838
+ _schedule_arbiter_poll(view, state)
556
839
  return True
557
840
  except Exception:
558
841
  return True
@@ -603,7 +886,7 @@ def _read_animated_value(view: Any, prop_name: str) -> Any:
603
886
  if prop_name == "rotate":
604
887
  return float(view.getRotation())
605
888
  except Exception:
606
- pass
889
+ diagnostics.swallowed("android._read_animated_value")
607
890
  return None
608
891
 
609
892
 
@@ -639,7 +922,7 @@ def _make_end_listener(anim_id: int) -> Any:
639
922
 
640
923
  native_animation_completed(anim_id, not self._cancelled)
641
924
  except Exception:
642
- pass
925
+ diagnostics.swallowed("android._make_end_listener._EndProxy.onAnimationEnd")
643
926
 
644
927
  return _EndProxy()
645
928
 
@@ -731,7 +1014,7 @@ class AndroidViewHandler(ViewHandler):
731
1014
  if parent is not None:
732
1015
  parent.removeView(native_view)
733
1016
  except Exception:
734
- pass
1017
+ diagnostics.swallowed("android.AndroidViewHandler.destroy")
735
1018
  _forget(native_view)
736
1019
 
737
1020
  def _build(self, props: Dict[str, Any]) -> Any:
@@ -760,17 +1043,24 @@ class AndroidViewHandler(ViewHandler):
760
1043
  lp.width = px_w
761
1044
  lp.height = px_h
762
1045
  except Exception:
763
- pass
1046
+ diagnostics.swallowed("android.AndroidViewHandler.set_frame")
764
1047
  try:
765
1048
  lp.leftMargin = px_x
766
1049
  lp.topMargin = px_y
767
1050
  lp.rightMargin = 0
768
1051
  lp.bottomMargin = 0
769
1052
  except Exception:
770
- pass
1053
+ diagnostics.swallowed("android.AndroidViewHandler.set_frame")
771
1054
  native_view.setLayoutParams(lp)
1055
+ # ``hit_slop`` needs the frame (and an attached parent, which
1056
+ # exists by the time frames are applied) to size its
1057
+ # TouchDelegate rect, so record the frame and recompute here.
1058
+ state = _state_of(native_view)
1059
+ state["frame"] = (x, y, width, height)
1060
+ if (state.get("props") or {}).get("hit_slop") is not None or state.get("hit_slop_active"):
1061
+ _update_hit_slop(native_view)
772
1062
  except Exception:
773
- pass
1063
+ diagnostics.swallowed("android.AndroidViewHandler.set_frame")
774
1064
 
775
1065
  def measure_intrinsic(
776
1066
  self,
@@ -801,7 +1091,13 @@ class AndroidViewHandler(ViewHandler):
801
1091
  return (0.0, 0.0)
802
1092
 
803
1093
  def set_animated_property(self, native_view: Any, prop_name: str, value: Any) -> None:
804
- """Apply one Python-driven animation frame immediately."""
1094
+ """Apply one Python-driven animation frame immediately.
1095
+
1096
+ Handles float transform props (dp-scaled translations, scales,
1097
+ rotation degrees, opacity) and interpolated color frames for
1098
+ ``background_color`` / ``color`` ("#AARRGGBB" / "#RRGGBB"
1099
+ strings or ints). Unknown props are silently ignored.
1100
+ """
805
1101
  if native_view is None:
806
1102
  return
807
1103
  try:
@@ -821,9 +1117,14 @@ class AndroidViewHandler(ViewHandler):
821
1117
  elif prop_name == "rotate":
822
1118
  native_view.setRotation(float(value))
823
1119
  elif prop_name == "background_color":
824
- native_view.setBackgroundColor(parse_color_int(value))
1120
+ _set_animated_background(native_view, value)
1121
+ elif prop_name == "color":
1122
+ # Interpolations emit "#AARRGGBB" strings; parse_color_int
1123
+ # accepts strings and raw ints alike. Views without
1124
+ # setTextColor fall into the except below and skip.
1125
+ native_view.setTextColor(parse_color_int(value))
825
1126
  except Exception:
826
- pass
1127
+ diagnostics.swallowed("android.AndroidViewHandler.set_animated_property")
827
1128
 
828
1129
  def start_animation(
829
1130
  self,
@@ -851,7 +1152,7 @@ class AndroidViewHandler(ViewHandler):
851
1152
  try:
852
1153
  entry["animator"].cancel()
853
1154
  except Exception:
854
- pass
1155
+ diagnostics.swallowed("android.AndroidViewHandler.cancel_animation")
855
1156
  return _read_animated_value(entry.get("view"), str(entry.get("prop", "")))
856
1157
 
857
1158
 
@@ -866,11 +1167,13 @@ class FlexContainerHandler(AndroidViewHandler):
866
1167
  All flex semantics (direction, alignment, distribution, padding)
867
1168
  are computed by the layout engine and applied via
868
1169
  [`set_frame`][pythonnative.native_views.android.AndroidViewHandler.set_frame].
869
- The container itself is just a positioning surface.
1170
+ The container itself is just a positioning surface (the template's
1171
+ ``PNFrameLayout`` when available, so ``pointer_events`` gets real
1172
+ touch interception).
870
1173
  """
871
1174
 
872
1175
  def _build(self, props: Dict[str, Any]) -> Any:
873
- return jclass("android.widget.FrameLayout")(_ctx())
1176
+ return _new_container()
874
1177
 
875
1178
  def insert_child(self, parent: Any, child: Any, index: int) -> None:
876
1179
  _insert_view(parent, child, index)
@@ -879,7 +1182,7 @@ class FlexContainerHandler(AndroidViewHandler):
879
1182
  try:
880
1183
  parent.removeView(child)
881
1184
  except Exception:
882
- pass
1185
+ diagnostics.swallowed("android.FlexContainerHandler.remove_child")
883
1186
 
884
1187
 
885
1188
  def _insert_view(parent: Any, child: Any, index: int) -> None:
@@ -905,7 +1208,7 @@ def _insert_view(parent: Any, child: Any, index: int) -> None:
905
1208
  try:
906
1209
  parent.addView(child)
907
1210
  except Exception:
908
- pass
1211
+ diagnostics.swallowed("android._insert_view")
909
1212
 
910
1213
 
911
1214
  # ======================================================================
@@ -993,7 +1296,7 @@ def _build_spannable(spans: Any) -> Any:
993
1296
  StrikethroughSpan = jclass("android.text.style.StrikethroughSpan")
994
1297
  _set(StrikethroughSpan())
995
1298
  except Exception:
996
- pass
1299
+ diagnostics.swallowed("android._build_spannable")
997
1300
  return builder
998
1301
 
999
1302
 
@@ -1035,7 +1338,7 @@ class TextHandler(AndroidViewHandler):
1035
1338
  else:
1036
1339
  tv.setTypeface(tv.getTypeface(), style)
1037
1340
  except Exception:
1038
- pass
1341
+ diagnostics.swallowed("android.TextHandler._apply")
1039
1342
  if "max_lines" in props and props["max_lines"] is not None:
1040
1343
  tv.setMaxLines(int(props["max_lines"]))
1041
1344
  if "text_align" in props:
@@ -1050,14 +1353,14 @@ class TextHandler(AndroidViewHandler):
1050
1353
  size = float(merged.get("font_size") or 16.0)
1051
1354
  tv.setLetterSpacing(float(props["letter_spacing"]) / max(size, 1.0))
1052
1355
  except Exception:
1053
- pass
1356
+ diagnostics.swallowed("android.TextHandler._apply")
1054
1357
  if "line_height" in props and props["line_height"] is not None:
1055
1358
  try:
1056
1359
  merged = _state_of(tv).get("props") or props
1057
1360
  size = float(merged.get("font_size") or 16.0)
1058
1361
  tv.setLineSpacing(0.0, float(props["line_height"]) / max(size, 1.0))
1059
1362
  except Exception:
1060
- pass
1363
+ diagnostics.swallowed("android.TextHandler._apply")
1061
1364
  if "text_decoration" in props:
1062
1365
  try:
1063
1366
  Paint = jclass("android.graphics.Paint")
@@ -1068,7 +1371,7 @@ class TextHandler(AndroidViewHandler):
1068
1371
  flags |= Paint.STRIKE_THRU_TEXT_FLAG
1069
1372
  tv.setPaintFlags(flags)
1070
1373
  except Exception:
1071
- pass
1374
+ diagnostics.swallowed("android.TextHandler._apply")
1072
1375
  _apply_common_visual(tv, props)
1073
1376
 
1074
1377
 
@@ -1147,7 +1450,7 @@ class ScrollViewHandler(AndroidViewHandler):
1147
1450
  try:
1148
1451
  outer.setEnabled(False)
1149
1452
  except Exception:
1150
- pass
1453
+ diagnostics.swallowed("android.ScrollViewHandler.create")
1151
1454
  self._bind_scroll_listener(outer, sv)
1152
1455
  self._apply(outer, props, initial=True)
1153
1456
  if props.get("gestures"):
@@ -1165,7 +1468,7 @@ class ScrollViewHandler(AndroidViewHandler):
1165
1468
  sv.setVerticalScrollBarEnabled(show)
1166
1469
  sv.setHorizontalScrollBarEnabled(show)
1167
1470
  except Exception:
1168
- pass
1471
+ diagnostics.swallowed("android.ScrollViewHandler._apply")
1169
1472
  if "bounces" in props:
1170
1473
  # ``bounces`` is only present when ``False``; map it to the
1171
1474
  # closest analogue, the over-scroll (glow) mode.
@@ -1174,7 +1477,7 @@ class ScrollViewHandler(AndroidViewHandler):
1174
1477
  mode = View.OVER_SCROLL_NEVER if props["bounces"] is False else View.OVER_SCROLL_IF_CONTENT_SCROLLS
1175
1478
  sv.setOverScrollMode(mode)
1176
1479
  except Exception:
1177
- pass
1480
+ diagnostics.swallowed("android.ScrollViewHandler._apply")
1178
1481
  if "refresh_control" in props and state.get("refresh") is not None:
1179
1482
  self._apply_refresh(outer, props)
1180
1483
  # ``paging_enabled`` and ``keyboard_dismiss_mode`` have no clean
@@ -1190,7 +1493,7 @@ class ScrollViewHandler(AndroidViewHandler):
1190
1493
  try:
1191
1494
  sv.removeView(child)
1192
1495
  except Exception:
1193
- pass
1496
+ diagnostics.swallowed("android.ScrollViewHandler.remove_child")
1194
1497
 
1195
1498
  def command(self, native_view: Any, name: str, args: Dict[str, Any]) -> Any:
1196
1499
  state = _state_of(native_view)
@@ -1206,7 +1509,7 @@ class ScrollViewHandler(AndroidViewHandler):
1206
1509
  else:
1207
1510
  sv.scrollTo(x_px, y_px)
1208
1511
  except Exception:
1209
- pass
1512
+ diagnostics.swallowed("android.ScrollViewHandler.command")
1210
1513
  return None
1211
1514
  if name == "scroll_to_end":
1212
1515
  try:
@@ -1220,7 +1523,7 @@ class ScrollViewHandler(AndroidViewHandler):
1220
1523
  target_y = max(0, int(child.getHeight()) - int(sv.getHeight()))
1221
1524
  sv.smoothScrollTo(0, target_y)
1222
1525
  except Exception:
1223
- pass
1526
+ diagnostics.swallowed("android.ScrollViewHandler.command")
1224
1527
  return None
1225
1528
  if name == "get_scroll_offset":
1226
1529
  try:
@@ -1250,11 +1553,13 @@ class ScrollViewHandler(AndroidViewHandler):
1250
1553
  density = _density() or 1.0
1251
1554
  _fire(outer, "on_scroll", {"x": scroll_x / density, "y": scroll_y / density})
1252
1555
  except Exception:
1253
- pass
1556
+ diagnostics.swallowed(
1557
+ "android.ScrollViewHandler._bind_scroll_listener._ScrollChangeProxy.onScrollChange"
1558
+ )
1254
1559
 
1255
1560
  sv.setOnScrollChangeListener(_ScrollChangeProxy())
1256
1561
  except Exception:
1257
- pass
1562
+ diagnostics.swallowed("android.ScrollViewHandler._bind_scroll_listener")
1258
1563
 
1259
1564
  def _wrap_in_refresh(self, sv: Any) -> Any:
1260
1565
  try:
@@ -1278,7 +1583,7 @@ class ScrollViewHandler(AndroidViewHandler):
1278
1583
 
1279
1584
  srl.setOnRefreshListener(_RefreshProxy())
1280
1585
  except Exception:
1281
- pass
1586
+ diagnostics.swallowed("android.ScrollViewHandler._bind_refresh_listener")
1282
1587
 
1283
1588
  def _apply_refresh(self, outer: Any, props: Dict[str, Any]) -> None:
1284
1589
  srl = _state_of(outer).get("refresh")
@@ -1289,21 +1594,21 @@ class ScrollViewHandler(AndroidViewHandler):
1289
1594
  try:
1290
1595
  srl.setEnabled(False)
1291
1596
  except Exception:
1292
- pass
1597
+ diagnostics.swallowed("android.ScrollViewHandler._apply_refresh")
1293
1598
  return
1294
1599
  try:
1295
1600
  srl.setEnabled(True)
1296
1601
  except Exception:
1297
- pass
1602
+ diagnostics.swallowed("android.ScrollViewHandler._apply_refresh")
1298
1603
  if spec.get("tint_color"):
1299
1604
  try:
1300
1605
  srl.setColorSchemeColors([parse_color_int(spec["tint_color"])])
1301
1606
  except Exception:
1302
- pass
1607
+ diagnostics.swallowed("android.ScrollViewHandler._apply_refresh")
1303
1608
  try:
1304
1609
  srl.setRefreshing(bool(spec.get("refreshing")))
1305
1610
  except Exception:
1306
- pass
1611
+ diagnostics.swallowed("android.ScrollViewHandler._apply_refresh")
1307
1612
 
1308
1613
 
1309
1614
  class TextInputHandler(AndroidViewHandler):
@@ -1316,7 +1621,7 @@ class TextInputHandler(AndroidViewHandler):
1316
1621
  if not props.get("multiline"):
1317
1622
  et.setSingleLine(True)
1318
1623
  except Exception:
1319
- pass
1624
+ diagnostics.swallowed("android.TextInputHandler._build")
1320
1625
  self._bind_listeners(et, props)
1321
1626
  return et
1322
1627
 
@@ -1342,7 +1647,7 @@ class TextInputHandler(AndroidViewHandler):
1342
1647
 
1343
1648
  et.addTextChangedListener(ChangeProxy())
1344
1649
  except Exception:
1345
- pass
1650
+ diagnostics.swallowed("android.TextInputHandler._bind_listeners")
1346
1651
  try:
1347
1652
 
1348
1653
  class _FocusProxy(dynamic_proxy(jclass("android.view.View").OnFocusChangeListener)):
@@ -1351,7 +1656,7 @@ class TextInputHandler(AndroidViewHandler):
1351
1656
 
1352
1657
  et.setOnFocusChangeListener(_FocusProxy())
1353
1658
  except Exception:
1354
- pass
1659
+ diagnostics.swallowed("android.TextInputHandler._bind_listeners")
1355
1660
 
1356
1661
  def _apply(self, et: Any, props: Dict[str, Any], initial: bool) -> None:
1357
1662
  state = _state_of(et)
@@ -1368,7 +1673,7 @@ class TextInputHandler(AndroidViewHandler):
1368
1673
  selection_start = et.getSelectionStart()
1369
1674
  selection_end = et.getSelectionEnd()
1370
1675
  except Exception:
1371
- pass
1676
+ diagnostics.swallowed("android.TextInputHandler._apply")
1372
1677
  state["suppress"] = True
1373
1678
  try:
1374
1679
  et.setText(incoming)
@@ -1381,7 +1686,7 @@ class TextInputHandler(AndroidViewHandler):
1381
1686
  else:
1382
1687
  et.setSelection(start, end)
1383
1688
  except Exception:
1384
- pass
1689
+ diagnostics.swallowed("android.TextInputHandler._apply")
1385
1690
  finally:
1386
1691
  state["suppress"] = False
1387
1692
  if "placeholder" in props:
@@ -1390,7 +1695,7 @@ class TextInputHandler(AndroidViewHandler):
1390
1695
  try:
1391
1696
  et.setHintTextColor(parse_color_int(props["placeholder_color"]))
1392
1697
  except Exception:
1393
- pass
1698
+ diagnostics.swallowed("android.TextInputHandler._apply")
1394
1699
  if "font_size" in props and props["font_size"] is not None:
1395
1700
  et.setTextSize(float(props["font_size"]))
1396
1701
  if "color" in props and props["color"] is not None:
@@ -1428,18 +1733,18 @@ class TextInputHandler(AndroidViewHandler):
1428
1733
  et.setSingleLine(True)
1429
1734
  et.setInputType(base)
1430
1735
  except Exception:
1431
- pass
1736
+ diagnostics.swallowed("android.TextInputHandler._apply")
1432
1737
  if "max_length" in props and props["max_length"] is not None:
1433
1738
  try:
1434
1739
  InputFilter = jclass("android.text.InputFilter$LengthFilter")
1435
1740
  et.setFilters([InputFilter(int(props["max_length"]))])
1436
1741
  except Exception:
1437
- pass
1742
+ diagnostics.swallowed("android.TextInputHandler._apply")
1438
1743
  if "auto_focus" in props and props["auto_focus"]:
1439
1744
  try:
1440
1745
  et.requestFocus()
1441
1746
  except Exception:
1442
- pass
1747
+ diagnostics.swallowed("android.TextInputHandler._apply")
1443
1748
  if "editable" in props:
1444
1749
  # ``editable`` is only present when ``False`` (read-only); a
1445
1750
  # removal (``None``) restores editing. We keep the field
@@ -1452,12 +1757,12 @@ class TextInputHandler(AndroidViewHandler):
1452
1757
  et.setCursorVisible(editable)
1453
1758
  et.setLongClickable(editable)
1454
1759
  except Exception:
1455
- pass
1760
+ diagnostics.swallowed("android.TextInputHandler._apply")
1456
1761
  if "selection_color" in props and props["selection_color"] is not None:
1457
1762
  try:
1458
1763
  et.setHighlightColor(parse_color_int(props["selection_color"]))
1459
1764
  except Exception:
1460
- pass
1765
+ diagnostics.swallowed("android.TextInputHandler._apply")
1461
1766
  if "text_content_type" in props and props["text_content_type"] is not None:
1462
1767
  self._apply_autofill(et, str(props["text_content_type"]))
1463
1768
  if "clear_button" in props:
@@ -1485,7 +1790,7 @@ class TextInputHandler(AndroidViewHandler):
1485
1790
  action = rkt_mapping.get(props["return_key_type"], EditorInfo.IME_ACTION_DONE)
1486
1791
  et.setImeOptions(action)
1487
1792
  except Exception:
1488
- pass
1793
+ diagnostics.swallowed("android.TextInputHandler._apply")
1489
1794
  if initial or "multiline" in props:
1490
1795
  self._apply_editor_action(et)
1491
1796
  _apply_common_visual(et, props)
@@ -1514,7 +1819,9 @@ class TextInputHandler(AndroidViewHandler):
1514
1819
  try:
1515
1820
  _fire(view, "on_submit", str(view.getText()))
1516
1821
  except Exception:
1517
- pass
1822
+ diagnostics.swallowed(
1823
+ "android.TextInputHandler._apply_editor_action.SubmitProxy.onEditorAction"
1824
+ )
1518
1825
  if not multiline:
1519
1826
  try:
1520
1827
  view.clearFocus()
@@ -1522,12 +1829,14 @@ class TextInputHandler(AndroidViewHandler):
1522
1829
  imm = ctx.getSystemService(Context.INPUT_METHOD_SERVICE)
1523
1830
  imm.hideSoftInputFromWindow(view.getWindowToken(), 0)
1524
1831
  except Exception:
1525
- pass
1832
+ diagnostics.swallowed(
1833
+ "android.TextInputHandler._apply_editor_action.SubmitProxy.onEditorAction"
1834
+ )
1526
1835
  return True
1527
1836
 
1528
1837
  et.setOnEditorActionListener(SubmitProxy())
1529
1838
  except Exception:
1530
- pass
1839
+ diagnostics.swallowed("android.TextInputHandler._apply_editor_action")
1531
1840
 
1532
1841
  @staticmethod
1533
1842
  def _autofill_hint(content_type: str) -> Optional[str]:
@@ -1559,7 +1868,7 @@ class TextInputHandler(AndroidViewHandler):
1559
1868
  if hint:
1560
1869
  et.setAutofillHints([hint])
1561
1870
  except Exception:
1562
- pass
1871
+ diagnostics.swallowed("android.TextInputHandler._apply_autofill")
1563
1872
 
1564
1873
  def _apply_clear_button(self, et: Any, enabled: bool) -> None:
1565
1874
  # Best-effort drawableEnd "X": shows a system clear icon and wires
@@ -1588,12 +1897,12 @@ class TextInputHandler(AndroidViewHandler):
1588
1897
  view.setText("")
1589
1898
  return True
1590
1899
  except Exception:
1591
- pass
1900
+ diagnostics.swallowed("android.TextInputHandler._apply_clear_button._ClearTouchProxy.onTouch")
1592
1901
  return False
1593
1902
 
1594
1903
  et.setOnTouchListener(_ClearTouchProxy())
1595
1904
  except Exception:
1596
- pass
1905
+ diagnostics.swallowed("android.TextInputHandler._apply_clear_button")
1597
1906
 
1598
1907
 
1599
1908
  class ImageHandler(AndroidViewHandler):
@@ -1606,12 +1915,12 @@ class ImageHandler(AndroidViewHandler):
1606
1915
  ColorStateList = jclass("android.content.res.ColorStateList")
1607
1916
  iv.setImageTintList(ColorStateList.valueOf(parse_color_int(props["tint_color"])))
1608
1917
  except Exception:
1609
- pass
1918
+ diagnostics.swallowed("android.ImageHandler._apply")
1610
1919
  if "placeholder_color" in props and props["placeholder_color"] is not None:
1611
1920
  try:
1612
1921
  iv.setBackgroundColor(parse_color_int(props["placeholder_color"]))
1613
1922
  except Exception:
1614
- pass
1923
+ diagnostics.swallowed("android.ImageHandler._apply")
1615
1924
  if "source" in props and props["source"]:
1616
1925
  self._load_source(iv, str(props["source"]))
1617
1926
  if "scale_type" in props and props["scale_type"]:
@@ -1648,7 +1957,7 @@ class ImageHandler(AndroidViewHandler):
1648
1957
  iv.setImageBitmap(bitmap)
1649
1958
  _fire(iv, "on_load")
1650
1959
  except Exception:
1651
- pass
1960
+ diagnostics.swallowed("android.ImageHandler._load_source._on_ready")
1652
1961
 
1653
1962
  def _on_error(message: str) -> None:
1654
1963
  if _state_of(iv).get("pending_uri") == source:
@@ -1674,7 +1983,7 @@ class ImageHandler(AndroidViewHandler):
1674
1983
  else:
1675
1984
  _fire(iv, "on_error", f"drawable {res_name!r} not found")
1676
1985
  except Exception:
1677
- pass
1986
+ diagnostics.swallowed("android.ImageHandler._load_source")
1678
1987
 
1679
1988
  def _load_data_uri(self, iv: Any, source: str) -> None:
1680
1989
  """Decode an inline ``data:`` URI (base64 payload) into the view."""
@@ -1719,7 +2028,7 @@ class ImageHandler(AndroidViewHandler):
1719
2028
  target_w = int(iv.getWidth())
1720
2029
  target_h = int(iv.getHeight())
1721
2030
  except Exception:
1722
- pass
2031
+ diagnostics.swallowed("android.ImageHandler._decode_downsampled")
1723
2032
  if target_w <= 0:
1724
2033
  try:
1725
2034
  metrics = _ctx().getResources().getDisplayMetrics()
@@ -1780,7 +2089,7 @@ class ProgressBarHandler(AndroidViewHandler):
1780
2089
  ColorStateList = jclass("android.content.res.ColorStateList")
1781
2090
  pb.setProgressTintList(ColorStateList.valueOf(parse_color_int(props["color"])))
1782
2091
  except Exception:
1783
- pass
2092
+ diagnostics.swallowed("android.ProgressBarHandler._apply")
1784
2093
  if "track_color" in props and props["track_color"] is not None:
1785
2094
  try:
1786
2095
  ColorStateList = jclass("android.content.res.ColorStateList")
@@ -1788,12 +2097,12 @@ class ProgressBarHandler(AndroidViewHandler):
1788
2097
  pb.setProgressBackgroundTintList(track)
1789
2098
  pb.setSecondaryProgressTintList(track)
1790
2099
  except Exception:
1791
- pass
2100
+ diagnostics.swallowed("android.ProgressBarHandler._apply")
1792
2101
  if "indeterminate" in props:
1793
2102
  try:
1794
2103
  pb.setIndeterminate(bool(props["indeterminate"]))
1795
2104
  except Exception:
1796
- pass
2105
+ diagnostics.swallowed("android.ProgressBarHandler._apply")
1797
2106
 
1798
2107
 
1799
2108
  class ActivityIndicatorHandler(AndroidViewHandler):
@@ -1809,7 +2118,7 @@ class ActivityIndicatorHandler(AndroidViewHandler):
1809
2118
  ColorStateList = jclass("android.content.res.ColorStateList")
1810
2119
  pb.setIndeterminateTintList(ColorStateList.valueOf(parse_color_int(props["color"])))
1811
2120
  except Exception:
1812
- pass
2121
+ diagnostics.swallowed("android.ActivityIndicatorHandler._apply")
1813
2122
  if "size" in props and props["size"] is not None:
1814
2123
  # The framework ProgressBar has no runtime size switch, so
1815
2124
  # approximate "large" by scaling the indeterminate drawable.
@@ -1818,7 +2127,7 @@ class ActivityIndicatorHandler(AndroidViewHandler):
1818
2127
  pb.setScaleX(scale)
1819
2128
  pb.setScaleY(scale)
1820
2129
  except Exception:
1821
- pass
2130
+ diagnostics.swallowed("android.ActivityIndicatorHandler._apply")
1822
2131
 
1823
2132
 
1824
2133
  def _make_web_client(wv: Any) -> Any:
@@ -1854,13 +2163,13 @@ def _make_web_client(wv: Any) -> Any:
1854
2163
  )
1855
2164
  view.evaluateJavascript(shim, None)
1856
2165
  except Exception:
1857
- pass
2166
+ diagnostics.swallowed("android._make_web_client._WebClientProxy.onPageFinished")
1858
2167
  inject_js = (_state_of(wv).get("props") or {}).get("inject_javascript")
1859
2168
  if inject_js:
1860
2169
  try:
1861
2170
  view.evaluateJavascript(str(inject_js), None)
1862
2171
  except Exception:
1863
- pass
2172
+ diagnostics.swallowed("android._make_web_client._WebClientProxy.onPageFinished")
1864
2173
 
1865
2174
  def shouldOverrideUrlLoading(self, view: Any, request: Any) -> bool:
1866
2175
  try:
@@ -1873,7 +2182,7 @@ def _make_web_client(wv: Any) -> Any:
1873
2182
 
1874
2183
  _fire(wv, "on_message", unquote(url[len(scheme) :]))
1875
2184
  except Exception:
1876
- pass
2185
+ diagnostics.swallowed("android._make_web_client._WebClientProxy.shouldOverrideUrlLoading")
1877
2186
  return True
1878
2187
  return False
1879
2188
 
@@ -1893,7 +2202,7 @@ class WebViewHandler(AndroidViewHandler):
1893
2202
  try:
1894
2203
  wv.getSettings().setJavaScriptEnabled(True)
1895
2204
  except Exception:
1896
- pass
2205
+ diagnostics.swallowed("android.WebViewHandler._apply")
1897
2206
 
1898
2207
  if initial:
1899
2208
  client = _make_web_client(wv)
@@ -1901,19 +2210,19 @@ class WebViewHandler(AndroidViewHandler):
1901
2210
  try:
1902
2211
  wv.setWebViewClient(client)
1903
2212
  except Exception:
1904
- pass
2213
+ diagnostics.swallowed("android.WebViewHandler._apply")
1905
2214
 
1906
2215
  # ``html`` takes precedence over ``url`` when both are present.
1907
2216
  if "html" in props and props["html"]:
1908
2217
  try:
1909
2218
  wv.loadDataWithBaseURL(None, str(props["html"]), "text/html", "utf-8", None)
1910
2219
  except Exception:
1911
- pass
2220
+ diagnostics.swallowed("android.WebViewHandler._apply")
1912
2221
  elif "url" in props and props["url"]:
1913
2222
  try:
1914
2223
  wv.loadUrl(str(props["url"]))
1915
2224
  except Exception:
1916
- pass
2225
+ diagnostics.swallowed("android.WebViewHandler._apply")
1917
2226
 
1918
2227
  if "scroll_enabled" in props:
1919
2228
  self._apply_scroll_enabled(wv, props["scroll_enabled"])
@@ -1930,7 +2239,7 @@ class WebViewHandler(AndroidViewHandler):
1930
2239
  else:
1931
2240
  wv.setOnTouchListener(None)
1932
2241
  except Exception:
1933
- pass
2242
+ diagnostics.swallowed("android.WebViewHandler._apply_scroll_enabled")
1934
2243
 
1935
2244
 
1936
2245
  class SpacerHandler(AndroidViewHandler):
@@ -1952,7 +2261,7 @@ class SafeAreaViewHandler(FlexContainerHandler):
1952
2261
  """Safe-area container using FrameLayout with ``fitsSystemWindows``."""
1953
2262
 
1954
2263
  def _build(self, props: Dict[str, Any]) -> Any:
1955
- fl = jclass("android.widget.FrameLayout")(_ctx())
2264
+ fl = _new_container()
1956
2265
  fl.setFitsSystemWindows(True)
1957
2266
  return fl
1958
2267
 
@@ -1995,7 +2304,7 @@ class ModalHandler(AndroidViewHandler):
1995
2304
  try:
1996
2305
  dialog.setCanceledOnTouchOutside(props["dismiss_on_backdrop"] is not False)
1997
2306
  except Exception:
1998
- pass
2307
+ diagnostics.swallowed("android.ModalHandler._apply")
1999
2308
 
2000
2309
  def insert_child(self, parent: Any, child: Any, index: int) -> None:
2001
2310
  state = _state_of(parent)
@@ -2012,7 +2321,7 @@ class ModalHandler(AndroidViewHandler):
2012
2321
  try:
2013
2322
  content.removeView(child)
2014
2323
  except Exception:
2015
- pass
2324
+ diagnostics.swallowed("android.ModalHandler.remove_child")
2016
2325
  else:
2017
2326
  buf = state.get("pending")
2018
2327
  if buf and child in buf:
@@ -2057,18 +2366,18 @@ class ModalHandler(AndroidViewHandler):
2057
2366
  WMLP = jclass("android.view.WindowManager$LayoutParams")
2058
2367
  window.setLayout(WMLP.MATCH_PARENT, WMLP.MATCH_PARENT)
2059
2368
  except Exception:
2060
- pass
2369
+ diagnostics.swallowed("android.ModalHandler._present")
2061
2370
  try:
2062
2371
  dialog.setCanceledOnTouchOutside(props.get("dismiss_on_backdrop") is not False)
2063
2372
  except Exception:
2064
- pass
2373
+ diagnostics.swallowed("android.ModalHandler._present")
2065
2374
  state["dialog"] = dialog
2066
2375
  state["content_view"] = content
2067
2376
  for child in state.pop("pending", []):
2068
2377
  try:
2069
2378
  content.addView(child)
2070
2379
  except Exception:
2071
- pass
2380
+ diagnostics.swallowed("android.ModalHandler._present")
2072
2381
 
2073
2382
  OnShowListener = jclass("android.content.DialogInterface$OnShowListener")
2074
2383
 
@@ -2087,7 +2396,7 @@ class ModalHandler(AndroidViewHandler):
2087
2396
  dialog.setOnDismissListener(_DismissProxy())
2088
2397
  dialog.show()
2089
2398
  except Exception:
2090
- pass
2399
+ diagnostics.swallowed("android.ModalHandler._present")
2091
2400
 
2092
2401
  def _dismiss(self, placeholder: Any, state: Dict[str, Any]) -> None:
2093
2402
  dialog = state.pop("dialog", None)
@@ -2096,7 +2405,7 @@ class ModalHandler(AndroidViewHandler):
2096
2405
  try:
2097
2406
  dialog.dismiss()
2098
2407
  except Exception:
2099
- pass
2408
+ diagnostics.swallowed("android.ModalHandler._dismiss")
2100
2409
 
2101
2410
 
2102
2411
  # ======================================================================
@@ -2148,7 +2457,7 @@ class PortalHandler(AndroidViewHandler):
2148
2457
  lp = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
2149
2458
  container.addView(overlay, lp)
2150
2459
  except Exception:
2151
- pass
2460
+ diagnostics.swallowed("android.PortalHandler._ensure_attached")
2152
2461
 
2153
2462
  def insert_child(self, parent: Any, child: Any, index: int) -> None:
2154
2463
  self._ensure_attached(parent)
@@ -2158,7 +2467,7 @@ class PortalHandler(AndroidViewHandler):
2158
2467
  try:
2159
2468
  parent.removeView(child)
2160
2469
  except Exception:
2161
- pass
2470
+ diagnostics.swallowed("android.PortalHandler.remove_child")
2162
2471
 
2163
2472
  def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
2164
2473
  # The overlay fills its container via MATCH_PARENT layout params;
@@ -2171,7 +2480,7 @@ class PortalHandler(AndroidViewHandler):
2171
2480
  if parent is not None:
2172
2481
  parent.removeView(overlay)
2173
2482
  except Exception:
2174
- pass
2483
+ diagnostics.swallowed("android.PortalHandler._teardown")
2175
2484
 
2176
2485
 
2177
2486
  class SliderHandler(AndroidViewHandler):
@@ -2225,7 +2534,7 @@ class TabBarHandler(AndroidViewHandler):
2225
2534
  try:
2226
2535
  bnv.setLabelVisibilityMode(self._LABEL_VISIBILITY_LABELED)
2227
2536
  except Exception:
2228
- pass
2537
+ diagnostics.swallowed("android.TabBarHandler._build")
2229
2538
  return bnv
2230
2539
  except Exception:
2231
2540
  LinearLayout = jclass("android.widget.LinearLayout")
@@ -2275,7 +2584,7 @@ class TabBarHandler(AndroidViewHandler):
2275
2584
 
2276
2585
  bnv.setOnItemSelectedListener(_TabSelectProxy())
2277
2586
  except Exception:
2278
- pass
2587
+ diagnostics.swallowed("android.TabBarHandler._bind_material_listener")
2279
2588
 
2280
2589
  def _set_menu(self, bnv: Any, items: list) -> None:
2281
2590
  try:
@@ -2289,9 +2598,9 @@ class TabBarHandler(AndroidViewHandler):
2289
2598
  try:
2290
2599
  menu_item.setIcon(res_id)
2291
2600
  except Exception:
2292
- pass
2601
+ diagnostics.swallowed("android.TabBarHandler._set_menu")
2293
2602
  except Exception:
2294
- pass
2603
+ diagnostics.swallowed("android.TabBarHandler._set_menu")
2295
2604
 
2296
2605
  def _resolve_icon(self, icon: Any) -> int:
2297
2606
  """Resolve a tab icon spec to an `android.R.drawable.*` res id.
@@ -2325,7 +2634,7 @@ class TabBarHandler(AndroidViewHandler):
2325
2634
  try:
2326
2635
  bnv.setSelectedItemId(i)
2327
2636
  except Exception:
2328
- pass
2637
+ diagnostics.swallowed("android.TabBarHandler._set_active")
2329
2638
  break
2330
2639
 
2331
2640
  def _apply_fallback(self, ll: Any, props: Dict[str, Any]) -> None:
@@ -2374,7 +2683,7 @@ class PressableHandler(FlexContainerHandler):
2374
2683
  _TAP_SLOP_DP = 12.0
2375
2684
 
2376
2685
  def _build(self, props: Dict[str, Any]) -> Any:
2377
- fl = jclass("android.widget.FrameLayout")(_ctx())
2686
+ fl = _new_container()
2378
2687
  fl.setClickable(True)
2379
2688
  fl.setFocusable(True)
2380
2689
  self._bind_press_stream(fl)
@@ -2403,6 +2712,8 @@ class PressableHandler(FlexContainerHandler):
2403
2712
 
2404
2713
  class _PressTouchProxy(dynamic_proxy(OnTouchListener)):
2405
2714
  def onTouch(self, view: Any, event: Any) -> bool:
2715
+ if _pointer_events_blocked(view):
2716
+ return False
2406
2717
  state = _state_of(view)
2407
2718
  _feed_motion_event(view, state, event)
2408
2719
  action = int(event.getActionMasked())
@@ -2425,7 +2736,9 @@ class PressableHandler(FlexContainerHandler):
2425
2736
  try:
2426
2737
  view.animate().alpha(opacity).setDuration(50).start()
2427
2738
  except Exception:
2428
- pass
2739
+ diagnostics.swallowed(
2740
+ "android.PressableHandler._bind_press_stream._PressTouchProxy.onTouch"
2741
+ )
2429
2742
  if _has_event(view, "on_long_press"):
2430
2743
  seq = press["seq"]
2431
2744
 
@@ -2456,7 +2769,9 @@ class PressableHandler(FlexContainerHandler):
2456
2769
  try:
2457
2770
  view.animate().alpha(1.0).setDuration(100).start()
2458
2771
  except Exception:
2459
- pass
2772
+ diagnostics.swallowed(
2773
+ "android.PressableHandler._bind_press_stream._PressTouchProxy.onTouch"
2774
+ )
2460
2775
  if action == 1 and not press["moved"] and not press["long_fired"]:
2461
2776
  _fire(view, "on_press")
2462
2777
  return True
@@ -2464,7 +2779,7 @@ class PressableHandler(FlexContainerHandler):
2464
2779
 
2465
2780
  fl.setOnTouchListener(_PressTouchProxy())
2466
2781
  except Exception:
2467
- pass
2782
+ diagnostics.swallowed("android.PressableHandler._bind_press_stream")
2468
2783
 
2469
2784
 
2470
2785
  # ======================================================================
@@ -2491,20 +2806,29 @@ class StatusBarHandler(AndroidViewHandler):
2491
2806
  return
2492
2807
  if "background_color" in props and props["background_color"] is not None:
2493
2808
  window.setStatusBarColor(parse_color_int(props["background_color"]))
2809
+ View = jclass("android.view.View")
2810
+ decor = window.getDecorView()
2494
2811
  if "bar_style" in props and props["bar_style"] is not None:
2495
2812
  # API 23+: setSystemUiVisibility with SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
2496
2813
  # for dark-content (light backgrounds), 0 for light-content.
2497
- View = jclass("android.view.View")
2498
2814
  bar_style = props["bar_style"]
2499
- decor = window.getDecorView()
2500
2815
  flags = decor.getSystemUiVisibility()
2501
2816
  if bar_style in ("dark", "default"):
2502
2817
  flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
2503
2818
  else:
2504
2819
  flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
2505
2820
  decor.setSystemUiVisibility(flags)
2506
- except Exception:
2507
- pass
2821
+ if "hidden" in props and props["hidden"] is not None:
2822
+ flags = decor.getSystemUiVisibility()
2823
+ if props["hidden"]:
2824
+ flags |= View.SYSTEM_UI_FLAG_FULLSCREEN
2825
+ else:
2826
+ flags &= ~View.SYSTEM_UI_FLAG_FULLSCREEN
2827
+ decor.setSystemUiVisibility(flags)
2828
+ except Exception as exc:
2829
+ from .. import diagnostics
2830
+
2831
+ diagnostics.warn_once(f"StatusBar: could not apply props on Android: {exc!r}")
2508
2832
 
2509
2833
 
2510
2834
  class KeyboardAvoidingViewHandler(FlexContainerHandler):
@@ -2552,7 +2876,7 @@ def _present_alert(
2552
2876
  try:
2553
2877
  on_result(index)
2554
2878
  except Exception:
2555
- pass
2879
+ diagnostics.swallowed("android._present_alert._deliver")
2556
2880
 
2557
2881
  def _present_on_main() -> None:
2558
2882
  try:
@@ -2738,7 +3062,7 @@ class CheckboxHandler(AndroidViewHandler):
2738
3062
  ColorStateList = jclass("android.content.res.ColorStateList")
2739
3063
  cb.setButtonTintList(ColorStateList.valueOf(parse_color_int(props["color"])))
2740
3064
  except Exception:
2741
- pass
3065
+ diagnostics.swallowed("android.CheckboxHandler._apply")
2742
3066
  _apply_accessibility(cb, props)
2743
3067
 
2744
3068
 
@@ -2798,7 +3122,7 @@ class SegmentedControlHandler(AndroidViewHandler):
2798
3122
  try:
2799
3123
  ll.removeAllViews()
2800
3124
  except Exception:
2801
- pass
3125
+ diagnostics.swallowed("android.SegmentedControlHandler._rebuild")
2802
3126
  state["buttons"] = []
2803
3127
  LL_LP = jclass("android.widget.LinearLayout$LayoutParams")
2804
3128
  restyle = self._restyle
@@ -2808,7 +3132,7 @@ class SegmentedControlHandler(AndroidViewHandler):
2808
3132
  try:
2809
3133
  btn.setAllCaps(False)
2810
3134
  except Exception:
2811
- pass
3135
+ diagnostics.swallowed("android.SegmentedControlHandler._rebuild")
2812
3136
  # Equal-width segments: zero base width + weight 1, full height.
2813
3137
  btn.setLayoutParams(LL_LP(0, LL_LP.MATCH_PARENT, 1.0))
2814
3138
  enabled = (state.get("props") or {}).get("enabled") is not False
@@ -2856,7 +3180,7 @@ class SegmentedControlHandler(AndroidViewHandler):
2856
3180
  btn.setBackground(drawable)
2857
3181
  btn.setEnabled(enabled)
2858
3182
  except Exception:
2859
- pass
3183
+ diagnostics.swallowed("android.SegmentedControlHandler._style_segment")
2860
3184
 
2861
3185
  def measure_intrinsic(
2862
3186
  self,
@@ -2911,7 +3235,7 @@ class DatePickerHandler(AndroidViewHandler):
2911
3235
  try:
2912
3236
  btn.setAllCaps(False)
2913
3237
  except Exception:
2914
- pass
3238
+ diagnostics.swallowed("android.DatePickerHandler._build")
2915
3239
  open_dialog = self._open_dialog
2916
3240
 
2917
3241
  class _DateTriggerProxy(dynamic_proxy(jclass("android.view.View").OnClickListener)):
@@ -3021,7 +3345,7 @@ class DatePickerHandler(AndroidViewHandler):
3021
3345
  if maximum:
3022
3346
  picker.setMaxDate(self._parse_to_calendar(maximum, mode).getTimeInMillis())
3023
3347
  except Exception:
3024
- pass
3348
+ diagnostics.swallowed("android.DatePickerHandler._apply_min_max")
3025
3349
 
3026
3350
  def _parse_to_calendar(self, value: Any, mode: str) -> Any:
3027
3351
  Calendar = jclass("java.util.Calendar")
@@ -3033,7 +3357,7 @@ class DatePickerHandler(AndroidViewHandler):
3033
3357
  fmt = SimpleDateFormat(self._PATTERNS.get(mode, self._PATTERNS["date"]), Locale.US)
3034
3358
  cal.setTime(fmt.parse(str(value)))
3035
3359
  except Exception:
3036
- pass
3360
+ diagnostics.swallowed("android.DatePickerHandler._parse_to_calendar")
3037
3361
  return cal
3038
3362
 
3039
3363
  def _format_calendar(self, cal: Any, mode: str) -> str:
@@ -3053,7 +3377,7 @@ class DatePickerHandler(AndroidViewHandler):
3053
3377
  try:
3054
3378
  btn.setText(iso)
3055
3379
  except Exception:
3056
- pass
3380
+ diagnostics.swallowed("android.DatePickerHandler._commit")
3057
3381
  _fire(btn, "on_change", iso)
3058
3382
 
3059
3383
 
@@ -3153,7 +3477,7 @@ class VirtualListHandler(AndroidViewHandler):
3153
3477
  try:
3154
3478
  info["pool"].release(_java_id(container))
3155
3479
  except Exception:
3156
- pass
3480
+ diagnostics.swallowed("android.VirtualListHandler._build._Delegate.onRowRecycled")
3157
3481
 
3158
3482
  def onScrolled(self, offset_dp: float, extent_dp: float, range_dp: float) -> None:
3159
3483
  view = info.get("view")
@@ -3202,12 +3526,12 @@ class VirtualListHandler(AndroidViewHandler):
3202
3526
  try:
3203
3527
  view.setVerticalScrollBarEnabled(show)
3204
3528
  except Exception:
3205
- pass
3529
+ diagnostics.swallowed("android.VirtualListHandler._apply")
3206
3530
  if data_changed and not initial:
3207
3531
  try:
3208
3532
  view.notifyDataChanged()
3209
3533
  except Exception:
3210
- pass
3534
+ diagnostics.swallowed("android.VirtualListHandler._apply")
3211
3535
 
3212
3536
  def _teardown(self, native_view: Any) -> None:
3213
3537
  info = _pn_vlist_info.pop(_java_id(native_view), None)
@@ -3238,7 +3562,7 @@ class VirtualListHandler(AndroidViewHandler):
3238
3562
  args.get("animated", True) is not False,
3239
3563
  )
3240
3564
  except Exception:
3241
- pass
3565
+ diagnostics.swallowed("android.VirtualListHandler.command")
3242
3566
  return None
3243
3567
  if name == "scroll_to_index":
3244
3568
  try:
@@ -3247,7 +3571,7 @@ class VirtualListHandler(AndroidViewHandler):
3247
3571
  args.get("animated", True) is not False,
3248
3572
  )
3249
3573
  except Exception:
3250
- pass
3574
+ diagnostics.swallowed("android.VirtualListHandler.command")
3251
3575
  return None
3252
3576
  if name == "scroll_to_end":
3253
3577
  info = _pn_vlist_info.get(_java_id(native_view))
@@ -3255,7 +3579,7 @@ class VirtualListHandler(AndroidViewHandler):
3255
3579
  try:
3256
3580
  native_view.scrollToIndex(max(0, count - 1), args.get("animated", True) is not False)
3257
3581
  except Exception:
3258
- pass
3582
+ diagnostics.swallowed("android.VirtualListHandler.command")
3259
3583
  return None
3260
3584
  if name == "get_scroll_offset":
3261
3585
  try: