pythonnative 0.26.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.
@@ -3,8 +3,10 @@
3
3
  [`NetInfo`][pythonnative.NetInfo] reports whether the device is online
4
4
  and over what kind of connection. ``fetch`` returns a snapshot dict;
5
5
  ``add_listener`` (and the [`use_net_info`][pythonnative.use_net_info]
6
- hook) deliver live updates as the native host forwards connectivity
7
- changes through
6
+ hook) deliver live updates: while at least one listener is subscribed,
7
+ a lightweight background watcher polls the platform's connectivity
8
+ state and dispatches a fresh snapshot whenever it changes. Native
9
+ hosts may also push changes directly through
8
10
  [`dispatch_net_info`][pythonnative.native_modules.net_info.dispatch_net_info].
9
11
 
10
12
  A snapshot looks like::
@@ -17,7 +19,8 @@ A snapshot looks like::
17
19
 
18
20
  from __future__ import annotations
19
21
 
20
- from typing import Callable, Dict, List
22
+ import threading
23
+ from typing import Callable, Dict, List, Optional
21
24
 
22
25
  from .. import diagnostics
23
26
  from ..hooks import use_effect, use_state
@@ -32,6 +35,11 @@ _last_state: NetInfoState = {
32
35
  "is_internet_reachable": True,
33
36
  }
34
37
 
38
+ # Background watcher driving live updates while listeners exist.
39
+ _WATCH_INTERVAL_S = 2.0
40
+ _watcher_lock = threading.Lock()
41
+ _watcher_stop: Optional[threading.Event] = None
42
+
35
43
 
36
44
  class NetInfo:
37
45
  """Network connectivity interface."""
@@ -48,18 +56,60 @@ class NetInfo:
48
56
 
49
57
  @staticmethod
50
58
  def add_listener(callback: Callable[[NetInfoState], None]) -> Callable[[], None]:
51
- """Subscribe to connectivity changes; returns an unsubscribe fn."""
59
+ """Subscribe to connectivity changes; returns an unsubscribe fn.
60
+
61
+ The first subscription starts a background watcher that polls
62
+ the platform connectivity state every couple of seconds and
63
+ dispatches a snapshot on change; the watcher stops when the
64
+ last listener unsubscribes.
65
+ """
52
66
  _listeners.append(callback)
67
+ _ensure_watcher()
53
68
 
54
69
  def _unsubscribe() -> None:
55
70
  try:
56
71
  _listeners.remove(callback)
57
72
  except ValueError:
58
73
  pass
74
+ if not _listeners:
75
+ _stop_watcher()
59
76
 
60
77
  return _unsubscribe
61
78
 
62
79
 
80
+ def _ensure_watcher() -> None:
81
+ """Start the connectivity watcher if a platform backend exists."""
82
+ global _watcher_stop
83
+ if not (IS_ANDROID or IS_IOS):
84
+ return
85
+ with _watcher_lock:
86
+ if _watcher_stop is not None:
87
+ return
88
+ stop = threading.Event()
89
+ _watcher_stop = stop
90
+
91
+ def _watch() -> None:
92
+ previous = dict(_last_state)
93
+ while not stop.wait(_WATCH_INTERVAL_S):
94
+ try:
95
+ current = _android_state() if IS_ANDROID else _ios_state()
96
+ except Exception:
97
+ continue
98
+ if current != previous:
99
+ previous = dict(current)
100
+ dispatch_net_info(current)
101
+
102
+ threading.Thread(target=_watch, daemon=True, name="pn-netinfo").start()
103
+
104
+
105
+ def _stop_watcher() -> None:
106
+ global _watcher_stop
107
+ with _watcher_lock:
108
+ if _watcher_stop is not None:
109
+ _watcher_stop.set()
110
+ _watcher_stop = None
111
+
112
+
63
113
  def dispatch_net_info(state: NetInfoState) -> None:
64
114
  """Push a new connectivity snapshot and notify listeners."""
65
115
  global _last_state
@@ -71,6 +121,25 @@ def dispatch_net_info(state: NetInfoState) -> None:
71
121
  diagnostics.swallowed("net_info.dispatch_net_info")
72
122
 
73
123
 
124
+ def dispatch_android_change() -> None:
125
+ """Re-read Android connectivity and notify listeners on change.
126
+
127
+ Called by the template's ``MainActivity`` from the
128
+ ``ConnectivityManager.NetworkCallback`` it registers via
129
+ ``registerDefaultNetworkCallback`` whenever the default network
130
+ appears, drops, or changes capabilities. The callback lives in
131
+ Kotlin because Chaquopy's ``dynamic_proxy`` implements interfaces
132
+ only and ``NetworkCallback`` is a class. Re-reading the snapshot
133
+ here keeps the transport-type mapping in one place, and identical
134
+ consecutive snapshots are dropped so paired ``onAvailable`` /
135
+ ``onCapabilitiesChanged`` callbacks don't double-notify.
136
+ """
137
+ state = _android_state()
138
+ if state == _last_state:
139
+ return
140
+ dispatch_net_info(state)
141
+
142
+
74
143
  def use_net_info() -> NetInfoState:
75
144
  """Subscribe a component to [`NetInfo`][pythonnative.NetInfo].
76
145
 
@@ -5,9 +5,10 @@ cancelling local push notifications. Uses Android's
5
5
  ``NotificationManager`` or iOS's ``UNUserNotificationCenter``.
6
6
 
7
7
  On iOS you must ``await Notifications.request_permission()`` before
8
- scheduling. On Android 13+ the runtime permission should be requested
9
- through standard Android APIs (the manifest declaration is otherwise
10
- sufficient).
8
+ scheduling. On Android 13+ the ``POST_NOTIFICATIONS`` runtime
9
+ permission is requested automatically the first time you schedule a
10
+ notification (the template manifest declares it); earlier Android
11
+ versions don't prompt at all.
11
12
 
12
13
  For remote (server-sent) pushes, enable the ``remote_notifications``
13
14
  capability in ``pythonnative.toml`` and call
@@ -55,17 +56,18 @@ class Notifications:
55
56
  async def request_permission() -> bool:
56
57
  """Request notification permission from the user.
57
58
 
58
- On Android the manifest declaration is normally sufficient for
59
- legacy permission grants and this returns ``True`` without
60
- prompting (the runtime POST_NOTIFICATIONS prompt for Android
61
- 13+ should be requested via standard Android APIs).
59
+ On Android 12 and below the manifest declaration is sufficient
60
+ and this returns ``True`` without prompting. On Android 13+
61
+ (API 33) the ``POST_NOTIFICATIONS`` runtime permission is
62
+ requested through [`Permissions`][pythonnative.Permissions],
63
+ which shows the system prompt if the user hasn't decided yet.
62
64
 
63
65
  Returns:
64
66
  ``True`` if granted (or no prompt is needed), ``False``
65
67
  otherwise.
66
68
  """
67
69
  if IS_ANDROID:
68
- return True
70
+ return await _android_request_permission()
69
71
  return await _ios_request_permission()
70
72
 
71
73
  @staticmethod
@@ -92,10 +94,15 @@ class Notifications:
92
94
 
93
95
  Returns:
94
96
  ``True`` on success, ``False`` if the underlying native
95
- call failed.
97
+ call failed or (on Android 13+) the user denied the
98
+ notification permission.
96
99
  """
97
100
  del options
98
101
  if IS_ANDROID:
102
+ # Android 13+ silently drops notifications posted without
103
+ # the POST_NOTIFICATIONS grant, so prompt before posting.
104
+ if not await _android_request_permission():
105
+ return False
99
106
  if delay_seconds > 0:
100
107
  _cancel_android_delayed(identifier)
101
108
  _android_delayed[identifier] = asyncio.ensure_future(
@@ -147,6 +154,27 @@ class Notifications:
147
154
  # ======================================================================
148
155
 
149
156
 
157
+ async def _android_request_permission() -> bool:
158
+ """Request ``POST_NOTIFICATIONS`` on Android 13+; older versions skip.
159
+
160
+ Delegates to the shared
161
+ [`Permissions`][pythonnative.Permissions] machinery, which routes
162
+ through the activity's ``requestPermissions`` and resolves once
163
+ ``onRequestPermissionsResult`` arrives. Below API 33 there's no
164
+ runtime permission, so this resolves ``True`` immediately.
165
+ """
166
+ try:
167
+ from java import jclass
168
+
169
+ if int(jclass("android.os.Build$VERSION").SDK_INT) < 33:
170
+ return True
171
+ except Exception:
172
+ return True
173
+ from .permissions import GRANTED, Permissions
174
+
175
+ return await Permissions.request("notifications") == GRANTED
176
+
177
+
150
178
  def _cancel_android_delayed(identifier: str) -> None:
151
179
  pending = _android_delayed.pop(identifier, None)
152
180
  if pending is not None and not pending.done():