pythonnative 0.24.0__py3-none-any.whl → 0.25.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.
@@ -28,11 +28,16 @@ Example:
28
28
  from __future__ import annotations
29
29
 
30
30
  import asyncio
31
- from typing import Any
31
+ from typing import Any, Dict
32
32
 
33
33
  from ..runtime import resolve_future
34
34
  from ..utils import IS_ANDROID
35
35
 
36
+ # Delayed Android deliveries in flight, keyed by identifier, so
37
+ # ``cancel`` can abort a notification that hasn't posted yet. (iOS
38
+ # delivers delays natively through UNTimeIntervalNotificationTrigger.)
39
+ _android_delayed: Dict[str, "asyncio.Task[Any]"] = {}
40
+
36
41
 
37
42
  class Notifications:
38
43
  """Local notification interface."""
@@ -82,7 +87,13 @@ class Notifications:
82
87
  """
83
88
  del options
84
89
  if IS_ANDROID:
85
- return await asyncio.to_thread(_android_schedule, title, body, delay_seconds, identifier)
90
+ if delay_seconds > 0:
91
+ _cancel_android_delayed(identifier)
92
+ _android_delayed[identifier] = asyncio.ensure_future(
93
+ _android_schedule_later(title, body, delay_seconds, identifier)
94
+ )
95
+ return True
96
+ return await asyncio.to_thread(_android_schedule, title, body, identifier)
86
97
  return await asyncio.to_thread(_ios_schedule, title, body, delay_seconds, identifier)
87
98
 
88
99
  @staticmethod
@@ -94,6 +105,7 @@ class Notifications:
94
105
  [`schedule`][pythonnative.native_modules.notifications.Notifications.schedule].
95
106
  """
96
107
  if IS_ANDROID:
108
+ _cancel_android_delayed(identifier)
97
109
  await asyncio.to_thread(_android_cancel, identifier)
98
110
  return
99
111
  await asyncio.to_thread(_ios_cancel, identifier)
@@ -104,8 +116,28 @@ class Notifications:
104
116
  # ======================================================================
105
117
 
106
118
 
107
- def _android_schedule(title: str, body: str, delay_seconds: float, identifier: str) -> bool:
108
- del delay_seconds # Android schedule is fire-and-forget for now.
119
+ def _cancel_android_delayed(identifier: str) -> None:
120
+ pending = _android_delayed.pop(identifier, None)
121
+ if pending is not None and not pending.done():
122
+ pending.cancel()
123
+
124
+
125
+ async def _android_schedule_later(title: str, body: str, delay_seconds: float, identifier: str) -> None:
126
+ """Post an Android notification after ``delay_seconds`` on the framework loop.
127
+
128
+ The delay lives in the app process (matching the semantics of
129
+ ``UNTimeIntervalNotificationTrigger`` closely enough for in-app
130
+ reminders); notifications whose delay outlives the process need
131
+ ``AlarmManager``, which is out of scope for the built-in module.
132
+ """
133
+ try:
134
+ await asyncio.sleep(delay_seconds)
135
+ await asyncio.to_thread(_android_schedule, title, body, identifier)
136
+ finally:
137
+ _android_delayed.pop(identifier, None)
138
+
139
+
140
+ def _android_schedule(title: str, body: str, identifier: str) -> bool:
109
141
  try:
110
142
  from java import jclass
111
143
 
@@ -193,8 +193,31 @@ def _ios_request(permission: str, on_done: Callable[[PermissionStatus], None]) -
193
193
 
194
194
  AVCaptureDevice.requestAccessForMediaType_completionHandler_(media, Block(_granted, None, bool))
195
195
  return
196
+
197
+ if permission == "photos":
198
+ PHPhotoLibrary = ObjCClass("PHPhotoLibrary")
199
+
200
+ def _photos_done(status: int) -> None:
201
+ # 0 notDetermined, 1 restricted, 2 denied, 3 authorized,
202
+ # 4 limited (counts as granted).
203
+ on_done({0: UNDETERMINED, 1: BLOCKED, 2: DENIED, 3: GRANTED, 4: GRANTED}.get(int(status), UNDETERMINED))
204
+
205
+ PHPhotoLibrary.requestAuthorization_(Block(_photos_done, None, int))
206
+ return
207
+
208
+ if permission == "notifications":
209
+ center = ObjCClass("UNUserNotificationCenter").currentNotificationCenter()
210
+
211
+ def _notif_done(granted: bool, _error: Any) -> None:
212
+ on_done(GRANTED if granted else DENIED)
213
+
214
+ # 0x07 = badge | sound | alert.
215
+ center.requestAuthorizationWithOptions_completionHandler_(0x07, Block(_notif_done, None, bool, object))
216
+ return
196
217
  except Exception:
197
218
  pass
198
219
  # Fall back to reporting the current status for permissions whose
199
- # request flow isn't wired up natively.
220
+ # request flow isn't wired up natively (location needs a
221
+ # CLLocationManager delegate; use the Location module's own
222
+ # request path for that).
200
223
  on_done(_ios_check(permission))
@@ -612,11 +612,16 @@ def _make_end_listener(anim_id: int) -> Any:
612
612
  AnimatorListener = jclass("android.animation.Animator$AnimatorListener")
613
613
 
614
614
  class _EndProxy(dynamic_proxy(AnimatorListener)):
615
+ # ``onAnimationStart`` / ``onAnimationEnd`` accept an optional
616
+ # ``isReverse``: API 26 added default two-arg overloads to
617
+ # ``AnimatorListener``, and ``ValueAnimator`` invokes those, so
618
+ # the java.lang.reflect.Proxy forwards two arguments here.
619
+
615
620
  def __init__(self) -> None:
616
621
  super().__init__()
617
622
  self._cancelled = False
618
623
 
619
- def onAnimationStart(self, animation: Any) -> None:
624
+ def onAnimationStart(self, animation: Any, isReverse: bool = False) -> None:
620
625
  pass
621
626
 
622
627
  def onAnimationRepeat(self, animation: Any) -> None:
@@ -625,7 +630,7 @@ def _make_end_listener(anim_id: int) -> Any:
625
630
  def onAnimationCancel(self, animation: Any) -> None:
626
631
  self._cancelled = True
627
632
 
628
- def onAnimationEnd(self, animation: Any) -> None:
633
+ def onAnimationEnd(self, animation: Any, isReverse: bool = False) -> None:
629
634
  entry = _native_anims.pop(anim_id, None)
630
635
  if entry is None:
631
636
  return
@@ -3251,7 +3251,18 @@ class PortalHandler(IOSViewHandler):
3251
3251
  return
3252
3252
  current = overlay.superview
3253
3253
  if current is not None and _objc_ptr(current) == _objc_ptr(host):
3254
- host.bringSubviewToFront_(overlay)
3254
+ # Already attached to the right host. Only reorder when
3255
+ # the overlay is not frontmost: reordering the subview
3256
+ # array during an active touch sequence (a re-render can
3257
+ # flush between touch-down and touch-up) can cancel the
3258
+ # touch and leave the portal's controls stuck
3259
+ # highlighted with their action never fired.
3260
+ try:
3261
+ front = host.subviews.lastObject()
3262
+ if front is None or _objc_ptr(front) != _objc_ptr(overlay):
3263
+ host.bringSubviewToFront_(overlay)
3264
+ except Exception:
3265
+ host.bringSubviewToFront_(overlay)
3255
3266
  self._sync_overlay_frame(overlay, host)
3256
3267
  return
3257
3268
  if current is not None: