pythonnative 0.26.0__py3-none-any.whl → 0.28.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.
- pythonnative/__init__.py +1 -1
- pythonnative/animated.py +697 -62
- pythonnative/cli/pn.py +7 -0
- pythonnative/components.py +61 -7
- pythonnative/gestures.py +446 -34
- pythonnative/native_modules/net_info.py +73 -4
- pythonnative/native_modules/notifications.py +37 -9
- pythonnative/native_views/android.py +320 -16
- pythonnative/native_views/desktop.py +766 -47
- pythonnative/native_views/ios.py +522 -33
- pythonnative/reconciler.py +28 -0
- pythonnative/style.py +19 -0
- pythonnative/templates/android_template/app/build.gradle +4 -0
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +4 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +60 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNFrameLayout.kt +50 -0
- pythonnative/templates/ios_template/ios_template/Info.plist +5 -0
- {pythonnative-0.26.0.dist-info → pythonnative-0.28.0.dist-info}/METADATA +3 -3
- {pythonnative-0.26.0.dist-info → pythonnative-0.28.0.dist-info}/RECORD +23 -22
- {pythonnative-0.26.0.dist-info → pythonnative-0.28.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.26.0.dist-info → pythonnative-0.28.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.26.0.dist-info → pythonnative-0.28.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.26.0.dist-info → pythonnative-0.28.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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():
|