pythonnative 0.35.0__py3-none-any.whl → 0.37.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 (176) hide show
  1. pythonnative/__init__.py +6 -4
  2. pythonnative/_ios_log.py +1 -1
  3. pythonnative/alerts.py +34 -88
  4. pythonnative/bootstrap.py +90 -0
  5. pythonnative/bridge/__init__.py +306 -0
  6. pythonnative/bridge/android.py +78 -0
  7. pythonnative/bridge/codec.py +164 -0
  8. pythonnative/bridge/fake.py +236 -0
  9. pythonnative/bridge/ios.py +138 -0
  10. pythonnative/cli/pn.py +24 -6
  11. pythonnative/components/layout.py +65 -3
  12. pythonnative/gestures.py +13 -10
  13. pythonnative/hooks.py +41 -26
  14. pythonnative/hosts/__init__.py +13 -36
  15. pythonnative/hosts/native.py +348 -0
  16. pythonnative/images.py +6 -5
  17. pythonnative/mutations.py +21 -0
  18. pythonnative/native_modules/__init__.py +30 -29
  19. pythonnative/native_modules/app_state.py +10 -6
  20. pythonnative/native_modules/battery.py +20 -84
  21. pythonnative/native_modules/biometrics.py +11 -124
  22. pythonnative/native_modules/camera.py +15 -291
  23. pythonnative/native_modules/clipboard.py +14 -109
  24. pythonnative/native_modules/desktop.py +322 -0
  25. pythonnative/native_modules/file_system.py +15 -21
  26. pythonnative/native_modules/haptics.py +19 -129
  27. pythonnative/native_modules/linking.py +24 -137
  28. pythonnative/native_modules/location.py +28 -180
  29. pythonnative/native_modules/net_info.py +25 -135
  30. pythonnative/native_modules/notifications.py +36 -257
  31. pythonnative/native_modules/permissions.py +20 -184
  32. pythonnative/native_modules/registry.py +513 -0
  33. pythonnative/native_modules/secure_store.py +18 -155
  34. pythonnative/native_modules/share.py +8 -113
  35. pythonnative/native_views/__init__.py +67 -86
  36. pythonnative/native_views/bridge_backend.py +264 -0
  37. pythonnative/platform.py +19 -15
  38. pythonnative/platform_metrics.py +3 -3
  39. pythonnative/project/android.py +0 -3
  40. pythonnative/project/builder.py +38 -16
  41. pythonnative/project/config.py +12 -2
  42. pythonnative/project/plugins.py +366 -0
  43. pythonnative/py.typed +0 -0
  44. pythonnative/runtime.py +35 -203
  45. pythonnative/sdk/__init__.py +48 -33
  46. pythonnative/sdk/_components.py +57 -76
  47. pythonnative/storage.py +18 -243
  48. pythonnative/templates/android_template/app/build.gradle +6 -9
  49. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
  50. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
  51. pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
  52. pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
  53. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
  54. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  55. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  56. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
  57. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  58. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
  59. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  60. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  61. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  62. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
  63. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  64. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
  65. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
  66. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  67. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  68. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  69. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  70. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  71. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
  72. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  73. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  74. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
  75. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  76. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  77. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  78. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  79. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  80. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  81. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
  82. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  83. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  84. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
  85. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  86. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  87. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  88. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  89. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  90. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  91. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
  92. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  93. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
  94. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  95. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
  96. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  97. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  98. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  99. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
  100. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
  101. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  102. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
  103. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
  104. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  105. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
  106. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
  107. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  108. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  109. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  110. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  111. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
  112. pythonnative/templates/android_template/settings.gradle +1 -0
  113. pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
  114. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
  115. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
  116. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
  117. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
  118. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  119. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
  120. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  121. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  122. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  123. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  124. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
  125. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  126. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  127. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  128. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  129. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
  130. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  131. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  132. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  133. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  134. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  135. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
  136. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  137. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  138. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  139. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
  140. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  141. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  142. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
  143. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
  144. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  145. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  146. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  147. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
  148. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  149. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  150. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  151. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  152. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  153. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  154. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
  155. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
  156. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  157. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
  158. pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
  159. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
  160. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
  161. pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
  162. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
  163. pythonnative/utils.py +4 -82
  164. pythonnative/virtual_rows.py +3 -3
  165. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
  166. pythonnative-0.37.0.dist-info/RECORD +263 -0
  167. pythonnative/hosts/android.py +0 -269
  168. pythonnative/hosts/ios.py +0 -404
  169. pythonnative/native_views/android.py +0 -3702
  170. pythonnative/native_views/ios.py +0 -5253
  171. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
  172. pythonnative-0.35.0.dist-info/RECORD +0 -155
  173. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
  174. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
  175. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
  176. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  """Haptic feedback and raw vibration.
2
2
 
3
- Two interfaces live here:
3
+ Two interfaces live here, both backed by the native ``Haptics`` module:
4
4
 
5
5
  - [`Haptics`][pythonnative.Haptics]: semantic, iOS-style feedback
6
6
  (impact / notification / selection) backed by
@@ -17,20 +17,20 @@ than errors.
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- from typing import Any, Optional
20
+ from typing import Any
21
21
 
22
22
  from .. import diagnostics
23
- from ..utils import IS_ANDROID, IS_IOS
23
+ from .registry import native_module
24
24
 
25
25
  ImpactStyle = str # "light" | "medium" | "heavy" | "soft" | "rigid"
26
26
  NotificationType = str # "success" | "warning" | "error"
27
27
 
28
- _IOS_IMPACT_STYLE = {"light": 0, "medium": 1, "heavy": 2, "soft": 4, "rigid": 5}
29
- _IOS_NOTIFICATION_TYPE = {"success": 0, "warning": 1, "error": 2}
30
28
 
31
- # Approximate Android fallback durations (ms) per semantic feedback.
32
- _ANDROID_IMPACT_MS = {"light": 10, "medium": 20, "heavy": 40, "soft": 15, "rigid": 30}
33
- _ANDROID_NOTIFICATION_MS = {"success": 30, "warning": 50, "error": 70}
29
+ def _call(method: str, **args: Any) -> None:
30
+ try:
31
+ native_module("Haptics").call(method, **args)
32
+ except Exception:
33
+ diagnostics.swallowed(f"haptics.{method}")
34
34
 
35
35
 
36
36
  class Haptics:
@@ -39,26 +39,17 @@ class Haptics:
39
39
  @staticmethod
40
40
  def impact(style: ImpactStyle = "medium") -> None:
41
41
  """Play a physical "impact" tap of the given ``style``."""
42
- if IS_IOS:
43
- _ios_impact(style)
44
- elif IS_ANDROID:
45
- _android_buzz(_ANDROID_IMPACT_MS.get(style, 20))
42
+ _call("impact", style=style)
46
43
 
47
44
  @staticmethod
48
45
  def notification(type_: NotificationType = "success") -> None:
49
46
  """Play a success / warning / error notification pattern."""
50
- if IS_IOS:
51
- _ios_notification(type_)
52
- elif IS_ANDROID:
53
- _android_buzz(_ANDROID_NOTIFICATION_MS.get(type_, 30))
47
+ _call("notification", type=type_)
54
48
 
55
49
  @staticmethod
56
50
  def selection() -> None:
57
51
  """Play the light "selection changed" tick."""
58
- if IS_IOS:
59
- _ios_selection()
60
- elif IS_ANDROID:
61
- _android_buzz(8)
52
+ _call("selection")
62
53
 
63
54
 
64
55
  class Vibration:
@@ -66,116 +57,15 @@ class Vibration:
66
57
 
67
58
  @staticmethod
68
59
  def vibrate(duration_ms: int = 400) -> None:
69
- """Vibrate for ``duration_ms`` milliseconds."""
70
- if IS_ANDROID:
71
- _android_buzz(int(duration_ms))
72
- elif IS_IOS:
73
- # iOS has no arbitrary-duration API; approximate with a
74
- # heavy impact for short buzzes and the legacy AudioServices
75
- # vibration for longer ones.
76
- if duration_ms >= 200:
77
- _ios_legacy_vibrate()
78
- else:
79
- _ios_impact("heavy")
60
+ """Vibrate for ``duration_ms`` milliseconds.
61
+
62
+ iOS has no arbitrary-duration API; the native module
63
+ approximates short buzzes with a heavy impact and longer ones
64
+ with the legacy system vibration sound.
65
+ """
66
+ _call("vibrate", duration_ms=int(duration_ms))
80
67
 
81
68
  @staticmethod
82
69
  def cancel() -> None:
83
70
  """Cancel an in-progress vibration (Android only)."""
84
- if IS_ANDROID:
85
- vibrator = _android_vibrator()
86
- if vibrator is not None:
87
- try:
88
- vibrator.cancel()
89
- except Exception:
90
- diagnostics.swallowed("haptics.Vibration.cancel")
91
-
92
-
93
- # ======================================================================
94
- # iOS
95
- # ======================================================================
96
-
97
-
98
- def _ios_impact(style: str) -> None:
99
- try:
100
- from rubicon.objc import ObjCClass
101
-
102
- generator = ObjCClass("UIImpactFeedbackGenerator").alloc().initWithStyle_(_IOS_IMPACT_STYLE.get(style, 1))
103
- generator.prepare()
104
- generator.impactOccurred()
105
- except Exception:
106
- diagnostics.swallowed("haptics._ios_impact")
107
-
108
-
109
- def _ios_notification(type_: str) -> None:
110
- try:
111
- from rubicon.objc import ObjCClass
112
-
113
- generator = ObjCClass("UINotificationFeedbackGenerator").alloc().init()
114
- generator.prepare()
115
- generator.notificationOccurred_(_IOS_NOTIFICATION_TYPE.get(type_, 0))
116
- except Exception:
117
- diagnostics.swallowed("haptics._ios_notification")
118
-
119
-
120
- def _ios_selection() -> None:
121
- try:
122
- from rubicon.objc import ObjCClass
123
-
124
- generator = ObjCClass("UISelectionFeedbackGenerator").alloc().init()
125
- generator.prepare()
126
- generator.selectionChanged()
127
- except Exception:
128
- diagnostics.swallowed("haptics._ios_selection")
129
-
130
-
131
- def _ios_legacy_vibrate() -> None:
132
- try:
133
- from ctypes import CDLL, util
134
-
135
- audio = CDLL(util.find_library("AudioToolbox"))
136
- audio.AudioServicesPlaySystemSound(4095) # kSystemSoundID_Vibrate
137
- except Exception:
138
- diagnostics.swallowed("haptics._ios_legacy_vibrate")
139
-
140
-
141
- # ======================================================================
142
- # Android
143
- # ======================================================================
144
-
145
-
146
- def _android_vibrator() -> Optional[Any]:
147
- try:
148
- from java import jclass
149
-
150
- from ..utils import get_android_context
151
-
152
- ctx = get_android_context()
153
- Build = jclass("android.os.Build")
154
- if Build.VERSION.SDK_INT >= 31:
155
- VibratorManager = jclass("android.os.VibratorManager")
156
- Context = jclass("android.content.Context")
157
- manager = ctx.getSystemService(Context.VIBRATOR_MANAGER_SERVICE)
158
- VibratorManager # referenced for clarity
159
- return manager.getDefaultVibrator()
160
- Context = jclass("android.content.Context")
161
- return ctx.getSystemService(Context.VIBRATOR_SERVICE)
162
- except Exception:
163
- return None
164
-
165
-
166
- def _android_buzz(duration_ms: int) -> None:
167
- vibrator = _android_vibrator()
168
- if vibrator is None:
169
- return
170
- try:
171
- from java import jclass
172
-
173
- Build = jclass("android.os.Build")
174
- if Build.VERSION.SDK_INT >= 26:
175
- VibrationEffect = jclass("android.os.VibrationEffect")
176
- effect = VibrationEffect.createOneShot(int(duration_ms), VibrationEffect.DEFAULT_AMPLITUDE)
177
- vibrator.vibrate(effect)
178
- else:
179
- vibrator.vibrate(int(duration_ms))
180
- except Exception:
181
- diagnostics.swallowed("haptics._android_buzz")
71
+ _call("cancel")
@@ -1,19 +1,21 @@
1
1
  """Open URLs, deep links, and the system settings page.
2
2
 
3
3
  [`Linking`][pythonnative.Linking] wraps ``UIApplication.openURL`` /
4
- ``Intent(ACTION_VIEW)`` so a Python app can hand a URL (``https:``,
5
- ``mailto:``, ``tel:``, a custom scheme, …) to the OS.
4
+ ``Intent(ACTION_VIEW)`` (in the native ``Linking`` module) so a Python
5
+ app can hand a URL (``https:``, ``mailto:``, ``tel:``, a custom
6
+ scheme, ...) to the OS.
6
7
 
7
8
  Outbound methods are synchronous and return a ``bool`` describing
8
9
  whether the platform accepted the request. On desktop they return
9
10
  ``False``.
10
11
 
11
12
  Inbound deep links flow the other way: declare your schemes in
12
- ``pythonnative.toml`` (``app.url_schemes``) and the native host calls
13
- [`dispatch_url`][pythonnative.native_modules.linking.dispatch_url] for
14
- every URL that opens the app. The URL that cold-started the app is kept
15
- and returned by ``get_initial_url``; later URLs reach subscribers added
16
- with ``add_listener``.
13
+ ``pythonnative.toml`` (``app.url_schemes``) and the native module
14
+ pushes a ``url`` event for every URL that opens the app, which lands in
15
+ [`dispatch_url`][pythonnative.native_modules.linking.dispatch_url]. The
16
+ URL that cold-started the app is kept and returned by
17
+ ``get_initial_url``; later URLs reach subscribers added with
18
+ ``add_listener``.
17
19
 
18
20
  Example:
19
21
  ```python
@@ -30,43 +32,36 @@ from __future__ import annotations
30
32
 
31
33
  from typing import Any, Callable, List, Optional
32
34
 
33
- from ..utils import IS_ANDROID, IS_IOS
35
+ from .registry import native_module, on_event
34
36
 
35
- # Populated by the native host when the app is launched from a deep
36
- # link; ``get_initial_url`` returns it.
37
37
  _initial_url: Optional[str] = None
38
38
  _url_listeners: List[Callable[[str], None]] = []
39
39
 
40
40
 
41
+ def _call_bool(method: str, **args: Any) -> bool:
42
+ try:
43
+ return bool(native_module("Linking").call(method, **args))
44
+ except Exception:
45
+ return False
46
+
47
+
41
48
  class Linking:
42
49
  """System URL / deep-link interface (synchronous)."""
43
50
 
44
51
  @staticmethod
45
52
  def open_url(url: str) -> bool:
46
53
  """Hand ``url`` to the OS. Returns ``True`` if it was accepted."""
47
- if IS_IOS:
48
- return _ios_open(url)
49
- if IS_ANDROID:
50
- return _android_open(url)
51
- return False
54
+ return _call_bool("open_url", url=url)
52
55
 
53
56
  @staticmethod
54
57
  def can_open_url(url: str) -> bool:
55
58
  """Return ``True`` when some installed app can handle ``url``."""
56
- if IS_IOS:
57
- return _ios_can_open(url)
58
- if IS_ANDROID:
59
- return _android_can_open(url)
60
- return False
59
+ return _call_bool("can_open_url", url=url)
61
60
 
62
61
  @staticmethod
63
62
  def open_settings() -> bool:
64
63
  """Open this app's entry in the system Settings app."""
65
- if IS_IOS:
66
- return _ios_open_settings()
67
- if IS_ANDROID:
68
- return _android_open_settings()
69
- return False
64
+ return _call_bool("open_settings")
70
65
 
71
66
  @staticmethod
72
67
  def get_initial_url() -> Optional[str]:
@@ -97,18 +92,17 @@ class Linking:
97
92
 
98
93
 
99
94
  def set_initial_url(url: Optional[str]) -> None:
100
- """Record the launch URL (called by the native host on cold start)."""
95
+ """Record the launch URL (or clear it with ``None``)."""
101
96
  global _initial_url
102
97
  _initial_url = url
103
98
 
104
99
 
105
100
  def dispatch_url(url: str) -> None:
106
- """Deliver an inbound deep link from the native host.
101
+ """Deliver an inbound deep link.
107
102
 
108
103
  The first URL ever dispatched is also recorded as the initial URL
109
104
  (a cold start from a deep link reaches Python only after the
110
- interpreter boots, so the host can't call ``set_initial_url``
111
- earlier than this).
105
+ interpreter boots, so native can't report it earlier than this).
112
106
 
113
107
  Args:
114
108
  url: The full URL string that opened the app.
@@ -125,111 +119,4 @@ def dispatch_url(url: str) -> None:
125
119
  diagnostics.warn(f"Linking listener raised while handling {url!r}: {exc!r}")
126
120
 
127
121
 
128
- # ======================================================================
129
- # iOS
130
- # ======================================================================
131
-
132
-
133
- def _ios_url(url: str) -> Optional[Any]:
134
- try:
135
- from rubicon.objc import ObjCClass
136
-
137
- return ObjCClass("NSURL").URLWithString_(url)
138
- except Exception:
139
- return None
140
-
141
-
142
- def _ios_open(url: str) -> bool:
143
- nsurl = _ios_url(url)
144
- if nsurl is None:
145
- return False
146
- try:
147
- from rubicon.objc import ObjCClass
148
-
149
- app = ObjCClass("UIApplication").sharedApplication
150
- if app.canOpenURL_(nsurl):
151
- app.openURL_options_completionHandler_(nsurl, None, None)
152
- return True
153
- return False
154
- except Exception:
155
- return False
156
-
157
-
158
- def _ios_can_open(url: str) -> bool:
159
- nsurl = _ios_url(url)
160
- if nsurl is None:
161
- return False
162
- try:
163
- from rubicon.objc import ObjCClass
164
-
165
- return bool(ObjCClass("UIApplication").sharedApplication.canOpenURL_(nsurl))
166
- except Exception:
167
- return False
168
-
169
-
170
- def _ios_open_settings() -> bool:
171
- try:
172
- from rubicon.objc import ObjCClass
173
-
174
- settings = ObjCClass("UIApplicationOpenSettingsURLString")
175
- return _ios_open(str(settings))
176
- except Exception:
177
- try:
178
- return _ios_open("app-settings:")
179
- except Exception:
180
- return False
181
-
182
-
183
- # ======================================================================
184
- # Android
185
- # ======================================================================
186
-
187
-
188
- def _android_open(url: str) -> bool:
189
- try:
190
- from java import jclass
191
-
192
- from ..utils import get_android_context
193
-
194
- Intent = jclass("android.content.Intent")
195
- Uri = jclass("android.net.Uri")
196
- intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
197
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
198
- get_android_context().startActivity(intent)
199
- return True
200
- except Exception:
201
- return False
202
-
203
-
204
- def _android_can_open(url: str) -> bool:
205
- try:
206
- from java import jclass
207
-
208
- from ..utils import get_android_context
209
-
210
- Intent = jclass("android.content.Intent")
211
- Uri = jclass("android.net.Uri")
212
- intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
213
- pm = get_android_context().getPackageManager()
214
- return intent.resolveActivity(pm) is not None
215
- except Exception:
216
- return False
217
-
218
-
219
- def _android_open_settings() -> bool:
220
- try:
221
- from java import jclass
222
-
223
- from ..utils import get_android_context
224
-
225
- Intent = jclass("android.content.Intent")
226
- Settings = jclass("android.provider.Settings")
227
- Uri = jclass("android.net.Uri")
228
- ctx = get_android_context()
229
- intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
230
- intent.setData(Uri.fromParts("package", ctx.getPackageName(), None))
231
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
232
- ctx.startActivity(intent)
233
- return True
234
- except Exception:
235
- return False
122
+ on_event("Linking", "url", lambda payload: dispatch_url(str(payload)))
@@ -3,6 +3,9 @@
3
3
  [`Location.get_current`][pythonnative.native_modules.location.Location.get_current]
4
4
  is a coroutine that resolves to a ``(latitude, longitude)`` tuple, or
5
5
  ``None`` if no recent fix is available or the user denies permission.
6
+ The native ``Location`` module owns the ``CLLocationManager`` /
7
+ ``LocationManager`` session and resolves the call with
8
+ ``{"latitude", "longitude", "accuracy", "altitude", "timestamp"}``.
6
9
 
7
10
  Permission prompts are triggered the first time a location-using API
8
11
  is called; ensure the appropriate manifest entries
@@ -24,16 +27,9 @@ Example:
24
27
 
25
28
  from __future__ import annotations
26
29
 
27
- import asyncio
28
- from typing import Any, Callable, Dict, Optional, Tuple
30
+ from typing import Any, Dict, Optional, Tuple
29
31
 
30
- from .. import diagnostics
31
- from ..runtime import resolve_future
32
- from ..utils import IS_ANDROID, IS_IOS
33
-
34
- # Retain pool keyed by ``id(delegate)`` so iOS / Android listeners
35
- # aren't garbage-collected before the platform calls back.
36
- _pending_delegates: Dict[int, Any] = {}
32
+ from .registry import native_module
37
33
 
38
34
  Coords = Tuple[float, float]
39
35
 
@@ -46,184 +42,36 @@ class Location:
46
42
  """Request the device's current location.
47
43
 
48
44
  Args:
49
- **options: Reserved for platform-specific tuning (e.g.,
50
- ``accuracy``, ``timeout``).
45
+ **options: Forwarded to the native module (``accuracy``,
46
+ ``timeout``). Unknown keys are ignored.
51
47
 
52
48
  Returns:
53
49
  ``(latitude, longitude)`` if a fix was obtained, otherwise
54
50
  ``None``.
55
51
  """
56
- del options
57
- loop = asyncio.get_running_loop()
58
- future: asyncio.Future[Optional[Coords]] = loop.create_future()
59
-
60
- def _on_result(coords: Optional[Coords]) -> None:
61
- resolve_future(future, coords)
62
-
63
- if IS_ANDROID:
64
- _android_get(_on_result)
65
- elif IS_IOS:
66
- _ios_get(_on_result)
67
- else:
68
- resolve_future(future, None)
69
-
70
- return await future
71
-
72
-
73
- # ======================================================================
74
- # iOS implementation: CLLocationManagerDelegate
75
- # ======================================================================
76
-
77
-
78
- def _ios_get(on_result: Callable[[Optional[Coords]], None]) -> None:
79
- try:
80
- from rubicon.objc import ObjCClass, objc_method
81
-
82
- CLLocationManager = ObjCClass("CLLocationManager")
83
- NSObject = ObjCClass("NSObject")
52
+ fix = await Location.get_current_fix(**options)
53
+ if fix is None:
54
+ return None
55
+ return (fix["latitude"], fix["longitude"])
84
56
 
85
- class _PNLocationDelegate(NSObject): # type: ignore[misc,valid-type]
86
- _callback: Optional[Callable[[Optional[Coords]], None]] = None
87
- _manager: Any = None
88
- _delivered: bool = False
89
-
90
- def _finish(self, coords: Optional[Coords]) -> None:
91
- if self._delivered:
92
- return
93
- self._delivered = True
94
- try:
95
- if self._manager is not None:
96
- self._manager.stopUpdatingLocation()
97
- except Exception:
98
- diagnostics.swallowed("location._ios_get._PNLocationDelegate._finish")
99
- cb = self._callback
100
- self._callback = None
101
- _pending_delegates.pop(id(self), None)
102
- if cb is not None:
103
- try:
104
- cb(coords)
105
- except Exception:
106
- diagnostics.swallowed("location._ios_get._PNLocationDelegate._finish")
107
-
108
- @objc_method
109
- def locationManager_didUpdateLocations_(self, manager: Any, locations: Any) -> None:
110
- del manager
111
- try:
112
- last = locations.lastObject
113
- if last is None:
114
- return
115
- coord = last.coordinate
116
- self._finish((float(coord.latitude), float(coord.longitude)))
117
- except Exception:
118
- self._finish(None)
119
-
120
- @objc_method
121
- def locationManager_didFailWithError_(self, manager: Any, error: Any) -> None:
122
- del manager, error
123
- self._finish(None)
124
-
125
- @objc_method
126
- def locationManagerDidChangeAuthorization_(self, manager: Any) -> None:
127
- try:
128
- status = int(manager.authorizationStatus)
129
- except Exception:
130
- status = 0
131
- if status in (3, 4): # AuthorizedAlways, AuthorizedWhenInUse
132
- try:
133
- manager.startUpdatingLocation()
134
- except Exception:
135
- diagnostics.swallowed(
136
- "location._ios_get._PNLocationDelegate.locationManagerDidChangeAuthorization_"
137
- )
138
-
139
- delegate = _PNLocationDelegate.new()
140
- delegate._callback = on_result
141
- _pending_delegates[id(delegate)] = delegate
142
-
143
- manager = CLLocationManager.alloc().init()
144
- manager.setDelegate_(delegate)
145
- delegate._manager = manager
146
- try:
147
- manager.requestWhenInUseAuthorization()
148
- except Exception:
149
- diagnostics.swallowed("location._ios_get")
150
- try:
151
- manager.startUpdatingLocation()
152
- except Exception:
153
- diagnostics.swallowed("location._ios_get")
154
- except Exception:
155
- on_result(None)
156
-
157
-
158
- # ======================================================================
159
- # Android implementation: LocationListener
160
- # ======================================================================
161
-
162
-
163
- def _android_get(on_result: Callable[[Optional[Coords]], None]) -> None:
164
- try:
165
- from java import dynamic_proxy, jclass
166
-
167
- from ..utils import get_android_context
168
-
169
- ctx = get_android_context()
170
- Context = jclass("android.content.Context")
171
- lm = ctx.getSystemService(Context.LOCATION_SERVICE)
57
+ @staticmethod
58
+ async def get_current_fix(**options: Any) -> Optional[Dict[str, float]]:
59
+ """Like ``get_current`` but returns the full fix dict.
172
60
 
173
- # Try the most recent known fix first; it's instant and avoids
174
- # the GPS warm-up delay.
61
+ Keys: ``latitude``, ``longitude``, and when the platform reports
62
+ them ``accuracy`` (meters), ``altitude`` (meters), ``speed``
63
+ (m/s), ``heading`` (degrees), ``timestamp`` (Unix seconds).
64
+ """
175
65
  try:
176
- for provider in ("gps", "network", "passive"):
177
- loc = lm.getLastKnownLocation(provider)
178
- if loc is not None:
179
- on_result((float(loc.getLatitude()), float(loc.getLongitude())))
180
- return
66
+ result = await native_module("Location").call_async("get_current", **options)
181
67
  except Exception:
182
- diagnostics.swallowed("location._android_get")
183
-
184
- LocationListener = jclass("android.location.LocationListener")
185
- delivered = [False]
186
-
187
- def _finish(coords: Optional[Coords], listener: Any) -> None:
188
- if delivered[0]:
189
- return
190
- delivered[0] = True
191
- try:
192
- lm.removeUpdates(listener)
193
- except Exception:
194
- diagnostics.swallowed("location._android_get._finish")
195
- _pending_delegates.pop(id(listener), None)
196
- try:
197
- on_result(coords)
198
- except Exception:
199
- diagnostics.swallowed("location._android_get._finish")
200
-
201
- class _PNLocationListener(dynamic_proxy(LocationListener)): # type: ignore[misc]
202
- def onLocationChanged(self, loc: Any) -> None:
203
- try:
204
- coords = (float(loc.getLatitude()), float(loc.getLongitude()))
205
- except Exception:
206
- coords = None
207
- _finish(coords, self)
208
-
209
- def onStatusChanged(self, provider: Any, status: int, extras: Any) -> None:
210
- del provider, status, extras
211
-
212
- def onProviderEnabled(self, provider: Any) -> None:
213
- del provider
214
-
215
- def onProviderDisabled(self, provider: Any) -> None:
216
- del provider
217
- _finish(None, self)
218
-
219
- listener = _PNLocationListener()
220
- _pending_delegates[id(listener)] = listener
68
+ return None
69
+ if not isinstance(result, dict):
70
+ return None
221
71
  try:
222
- lm.requestSingleUpdate("gps", listener, None)
223
- except Exception:
224
- try:
225
- lm.requestLocationUpdates("network", 1000, 0.0, listener)
226
- except Exception:
227
- _finish(None, listener)
228
- except Exception:
229
- on_result(None)
72
+ fix: Dict[str, float] = {k: float(v) for k, v in result.items() if v is not None}
73
+ fix["latitude"]
74
+ fix["longitude"]
75
+ except (KeyError, TypeError, ValueError):
76
+ return None
77
+ return fix