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
@@ -2,16 +2,18 @@
2
2
 
3
3
  [`Battery`][pythonnative.Battery] reports the current charge fraction
4
4
  (``0.0``–``1.0``, or ``-1.0`` when unknown) and charging state, and
5
- lets you subscribe to changes that the native host forwards via
5
+ lets you subscribe to changes. The native ``Battery`` module pushes a
6
+ ``change`` event with ``{"level", "state"}``; off device, tests drive
7
+ the same path through
6
8
  [`dispatch_battery`][pythonnative.native_modules.battery.dispatch_battery].
7
9
  """
8
10
 
9
11
  from __future__ import annotations
10
12
 
11
- from typing import Callable, Dict, List
13
+ from typing import Any, Callable, Dict, List
12
14
 
13
15
  from .. import diagnostics
14
- from ..utils import IS_ANDROID, IS_IOS
16
+ from .registry import native_module, on_event
15
17
 
16
18
  BatteryState = str # "unknown" | "unplugged" | "charging" | "full"
17
19
 
@@ -24,20 +26,20 @@ class Battery:
24
26
  @staticmethod
25
27
  def get_level() -> float:
26
28
  """Return the charge fraction in ``[0, 1]`` (``-1.0`` if unknown)."""
27
- if IS_IOS:
28
- return _ios_level()
29
- if IS_ANDROID:
30
- return _android_level()
31
- return -1.0
29
+ try:
30
+ level = float(native_module("Battery").call("get_level"))
31
+ except Exception:
32
+ return -1.0
33
+ return level if 0.0 <= level <= 1.0 else -1.0
32
34
 
33
35
  @staticmethod
34
36
  def get_state() -> BatteryState:
35
37
  """Return ``"charging"`` / ``"full"`` / ``"unplugged"`` / ``"unknown"``."""
36
- if IS_IOS:
37
- return _ios_state()
38
- if IS_ANDROID:
39
- return _android_state()
40
- return "unknown"
38
+ try:
39
+ state = str(native_module("Battery").call("get_state") or "unknown")
40
+ except Exception:
41
+ return "unknown"
42
+ return state if state in ("unknown", "unplugged", "charging", "full") else "unknown"
41
43
 
42
44
  @staticmethod
43
45
  def add_listener(callback: Callable[[Dict[str, object]], None]) -> Callable[[], None]:
@@ -57,7 +59,7 @@ class Battery:
57
59
 
58
60
 
59
61
  def dispatch_battery(level: float, state: BatteryState) -> None:
60
- """Notify listeners of a battery change (called by the native host)."""
62
+ """Notify listeners of a battery change."""
61
63
  payload: Dict[str, object] = {"level": level, "state": state}
62
64
  for listener in list(_listeners):
63
65
  try:
@@ -66,75 +68,9 @@ def dispatch_battery(level: float, state: BatteryState) -> None:
66
68
  diagnostics.swallowed("battery.dispatch_battery")
67
69
 
68
70
 
69
- # ======================================================================
70
- # iOS: UIDevice
71
- # ======================================================================
72
-
73
-
74
- def _ios_device() -> object:
75
- from rubicon.objc import ObjCClass
76
-
77
- device = ObjCClass("UIDevice").currentDevice
78
- try:
79
- device.setBatteryMonitoringEnabled_(True)
80
- except Exception:
81
- diagnostics.swallowed("battery._ios_device")
82
- return device
83
-
84
-
85
- def _ios_level() -> float:
86
- try:
87
- level = float(_ios_device().batteryLevel)
88
- return level if level >= 0 else -1.0
89
- except Exception:
90
- return -1.0
91
-
92
-
93
- def _ios_state() -> BatteryState:
94
- try:
95
- # UIDeviceBatteryState: 0 unknown, 1 unplugged, 2 charging, 3 full
96
- return {0: "unknown", 1: "unplugged", 2: "charging", 3: "full"}.get(int(_ios_device().batteryState), "unknown")
97
- except Exception:
98
- return "unknown"
99
-
100
-
101
- # ======================================================================
102
- # Android: BatteryManager
103
- # ======================================================================
104
-
105
-
106
- def _android_level() -> float:
107
- try:
108
- from java import jclass
109
-
110
- from ..utils import get_android_context
111
-
112
- ctx = get_android_context()
113
- Context = jclass("android.content.Context")
114
- bm = ctx.getSystemService(Context.BATTERY_SERVICE)
115
- BatteryManager = jclass("android.os.BatteryManager")
116
- pct = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
117
- return (pct / 100.0) if pct is not None and pct >= 0 else -1.0
118
- except Exception:
119
- return -1.0
120
-
121
-
122
- def _android_state() -> BatteryState:
123
- try:
124
- from java import jclass
71
+ def _on_native_change(payload: Any) -> None:
72
+ if isinstance(payload, dict):
73
+ dispatch_battery(float(payload.get("level", -1.0)), str(payload.get("state", "unknown")))
125
74
 
126
- from ..utils import get_android_context
127
75
 
128
- ctx = get_android_context()
129
- Context = jclass("android.content.Context")
130
- bm = ctx.getSystemService(Context.BATTERY_SERVICE)
131
- BatteryManager = jclass("android.os.BatteryManager")
132
- status = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS)
133
- return {
134
- BatteryManager.BATTERY_STATUS_CHARGING: "charging",
135
- BatteryManager.BATTERY_STATUS_FULL: "full",
136
- BatteryManager.BATTERY_STATUS_DISCHARGING: "unplugged",
137
- BatteryManager.BATTERY_STATUS_NOT_CHARGING: "unplugged",
138
- }.get(status, "unknown")
139
- except Exception:
140
- return "unknown"
76
+ on_event("Battery", "change", _on_native_change)
@@ -2,7 +2,8 @@
2
2
 
3
3
  [`Biometrics`][pythonnative.Biometrics] gates an action behind the
4
4
  device's biometric hardware via ``LAContext`` (iOS) and
5
- ``BiometricPrompt`` (Android).
5
+ ``BiometricPrompt`` (Android), both implemented in the native
6
+ ``Biometrics`` module.
6
7
 
7
8
  ``is_available`` is synchronous; ``authenticate`` is a coroutine that
8
9
  presents the system prompt and resolves to ``True`` on success or
@@ -20,14 +21,7 @@ Example:
20
21
 
21
22
  from __future__ import annotations
22
23
 
23
- import asyncio
24
- from typing import Any, Callable, Dict
25
-
26
- from .. import diagnostics
27
- from ..runtime import resolve_future
28
- from ..utils import IS_ANDROID, IS_IOS
29
-
30
- _pending: Dict[int, Any] = {}
24
+ from .registry import native_module
31
25
 
32
26
 
33
27
  class Biometrics:
@@ -36,122 +30,15 @@ class Biometrics:
36
30
  @staticmethod
37
31
  def is_available() -> bool:
38
32
  """Return ``True`` when biometric auth can be attempted."""
39
- if IS_IOS:
40
- return _ios_available()
41
- if IS_ANDROID:
42
- return _android_available()
43
- return False
33
+ try:
34
+ return bool(native_module("Biometrics").call("is_available"))
35
+ except Exception:
36
+ return False
44
37
 
45
38
  @staticmethod
46
39
  async def authenticate(reason: str = "Authenticate") -> bool:
47
40
  """Present the biometric prompt; resolve ``True`` on success."""
48
- loop = asyncio.get_running_loop()
49
- future: asyncio.Future[bool] = loop.create_future()
50
-
51
- def _done(ok: bool) -> None:
52
- resolve_future(future, ok)
53
-
54
- if IS_IOS:
55
- _ios_authenticate(reason, _done)
56
- elif IS_ANDROID:
57
- _android_authenticate(reason, _done)
58
- else:
59
- _done(False)
60
-
61
- return await future
62
-
63
-
64
- # ======================================================================
65
- # iOS: LAContext
66
- # ======================================================================
67
-
68
-
69
- def _ios_context() -> Any:
70
- from rubicon.objc import ObjCClass
71
-
72
- return ObjCClass("LAContext").alloc().init()
73
-
74
-
75
- def _ios_available() -> bool:
76
- try:
77
- # LAPolicyDeviceOwnerAuthenticationWithBiometrics == 1
78
- return bool(_ios_context().canEvaluatePolicy_error_(1, None))
79
- except Exception:
80
- return False
81
-
82
-
83
- def _ios_authenticate(reason: str, on_done: Callable[[bool], None]) -> None:
84
- try:
85
- from rubicon.objc import Block, ObjCClass
86
-
87
- context = _ios_context()
88
- token = id(context)
89
- _pending[token] = context
90
-
91
- def _reply(success: bool, error: Any) -> None:
92
- del error
93
- _pending.pop(token, None)
94
- try:
95
- on_done(bool(success))
96
- except Exception:
97
- diagnostics.swallowed("biometrics._ios_authenticate._reply")
98
-
99
- context.evaluatePolicy_localizedReason_reply_(1, reason, Block(_reply, None, bool, ObjCClass("NSError")))
100
- except Exception:
101
- on_done(False)
102
-
103
-
104
- # ======================================================================
105
- # Android: BiometricPrompt
106
- # ======================================================================
107
-
108
-
109
- def _android_available() -> bool:
110
- try:
111
- from java import jclass
112
-
113
- from ..utils import get_android_context
114
-
115
- BiometricManager = jclass("androidx.biometric.BiometricManager")
116
- manager = BiometricManager.from_(get_android_context())
117
- Authenticators = jclass("androidx.biometric.BiometricManager$Authenticators")
118
- result = manager.canAuthenticate(Authenticators.BIOMETRIC_WEAK)
119
- return result == BiometricManager.BIOMETRIC_SUCCESS
120
- except Exception:
121
- return False
122
-
123
-
124
- def _android_authenticate(reason: str, on_done: Callable[[bool], None]) -> None:
125
- try:
126
- from java import dynamic_proxy, jclass
127
-
128
- from ..utils import get_android_context
129
-
130
- ctx = get_android_context()
131
- FragmentActivity = jclass("androidx.fragment.app.FragmentActivity")
132
- if not FragmentActivity.isInstance(ctx):
133
- on_done(False)
134
- return
135
-
136
- BiometricPrompt = jclass("androidx.biometric.BiometricPrompt")
137
- ContextCompat = jclass("androidx.core.content.ContextCompat")
138
- executor = ContextCompat.getMainExecutor(ctx)
139
-
140
- class _Callback(dynamic_proxy(BiometricPrompt.AuthenticationCallback)): # type: ignore[misc]
141
- def onAuthenticationSucceeded(self, result: Any) -> None: # noqa: N802, ARG002
142
- on_done(True)
143
-
144
- def onAuthenticationError(self, code: int, message: Any) -> None: # noqa: N802, ARG002
145
- on_done(False)
146
-
147
- def onAuthenticationFailed(self) -> None: # noqa: N802
148
- # Fired on a non-matching attempt; the prompt stays up,
149
- # so don't resolve yet.
150
- pass
151
-
152
- prompt = BiometricPrompt(ctx, executor, _Callback())
153
- Builder = jclass("androidx.biometric.BiometricPrompt$PromptInfo$Builder")
154
- info = Builder().setTitle(reason).setNegativeButtonText("Cancel").build()
155
- prompt.authenticate(info)
156
- except Exception:
157
- on_done(False)
41
+ try:
42
+ return bool(await native_module("Biometrics").call_async("authenticate", reason=reason))
43
+ except Exception:
44
+ return False
@@ -1,12 +1,11 @@
1
1
  """Cross-platform camera and gallery access.
2
2
 
3
3
  Both entry points are coroutines: ``await Camera.take_photo()`` returns
4
- the saved image path (a ``str``) or ``None`` if the user cancels.
5
- Internally each call instantiates a fresh native delegate / activity
6
- request and bridges its completion onto the PythonNative asyncio
7
- runtime, so callers don't have to know whether the picker is backed by
8
- ``UIImagePickerController`` (iOS) or
9
- ``Intent(MediaStore.ACTION_IMAGE_CAPTURE)`` (Android).
4
+ the saved image path (a ``str``) or ``None`` if the user cancels. The
5
+ native ``Camera`` module presents ``UIImagePickerController`` (iOS) or
6
+ launches ``MediaStore.ACTION_IMAGE_CAPTURE`` / ``ACTION_PICK``
7
+ (Android) and resolves the call when the picker finishes; Python only
8
+ awaits the promise.
10
9
 
11
10
  Example:
12
11
  ```python
@@ -22,316 +21,41 @@ Example:
22
21
 
23
22
  from __future__ import annotations
24
23
 
25
- import asyncio
26
- from typing import Any, Callable, Dict, Optional
24
+ from typing import Any, Optional
27
25
 
28
- from .. import diagnostics
29
- from ..runtime import resolve_future
30
- from ..utils import IS_ANDROID, IS_IOS
31
-
32
- # Retain pool keyed by ``id(delegate)`` so iOS UIImagePickerController
33
- # delegates aren't garbage-collected before the picker calls back.
34
- _pending_delegates: Dict[int, Any] = {}
26
+ from .registry import native_module
35
27
 
36
28
 
37
29
  class Camera:
38
- """Camera and image-picker interface.
39
-
40
- All methods are static coroutines. They dispatch to the iOS or
41
- Android implementation at call time based on the runtime
42
- platform.
43
- """
30
+ """Camera and image-picker interface (static coroutines)."""
44
31
 
45
32
  @staticmethod
46
33
  async def take_photo(**options: Any) -> Optional[str]:
47
34
  """Launch the device camera to capture a photo.
48
35
 
49
36
  Args:
50
- **options: Reserved for platform-specific tuning. Currently
51
- unused; future kwargs (e.g., ``quality``,
52
- ``flash_mode``) will land here.
37
+ **options: Forwarded to the native module (``quality``,
38
+ ``allow_editing``, ...). Unknown keys are ignored.
53
39
 
54
40
  Returns:
55
41
  The saved image path, or ``None`` if the user cancelled or
56
42
  no camera is available.
57
43
  """
58
- return await _launch("camera", **options)
44
+ return await _launch("take_photo", options)
59
45
 
60
46
  @staticmethod
61
47
  async def pick_from_gallery(**options: Any) -> Optional[str]:
62
48
  """Open the system gallery picker.
63
49
 
64
- Args:
65
- **options: Reserved for platform-specific tuning.
66
-
67
50
  Returns:
68
51
  The selected image path, or ``None`` if the user cancelled.
69
52
  """
70
- return await _launch("gallery", **options)
71
-
72
-
73
- async def _launch(source: str, **options: Any) -> Optional[str]:
74
- del options
75
- loop = asyncio.get_running_loop()
76
- future: asyncio.Future[Optional[str]] = loop.create_future()
77
-
78
- def _on_result(path: Optional[str]) -> None:
79
- resolve_future(future, path)
80
-
81
- if IS_ANDROID:
82
- _android_launch_picker(_on_result, source=source)
83
- elif IS_IOS:
84
- _ios_launch_picker(_on_result, source=source)
85
- else:
86
- resolve_future(future, None)
87
-
88
- return await future
89
-
90
-
91
- # ======================================================================
92
- # iOS implementation: UIImagePickerControllerDelegate
93
- # ======================================================================
94
-
95
-
96
- def _ios_launch_picker(on_result: Callable[[Optional[str]], None], source: str) -> None:
97
- try:
98
- from rubicon.objc import SEL, ObjCClass, objc_method
99
-
100
- UIImagePickerController = ObjCClass("UIImagePickerController")
101
- NSObject = ObjCClass("NSObject")
102
-
103
- class _PNImagePickerDelegate(NSObject): # type: ignore[misc,valid-type]
104
- _callback: Optional[Callable[[Optional[str]], None]] = None
105
- _picker: Any = None
106
-
107
- @objc_method
108
- def imagePickerController_didFinishPickingMediaWithInfo_(self, picker: Any, info: Any) -> None:
109
- path = _ios_extract_path(info)
110
- try:
111
- picker.dismissViewControllerAnimated_completion_(True, None)
112
- except Exception:
113
- diagnostics.swallowed(
114
- "camera._ios_launch_picker._PNImagePickerDelegate.imagePickerController_didFinishPickingMediaWithInfo_"
115
- )
116
- cb = self._callback
117
- self._callback = None
118
- _pending_delegates.pop(id(self), None)
119
- if cb is not None:
120
- try:
121
- cb(path)
122
- except Exception:
123
- diagnostics.swallowed(
124
- "camera._ios_launch_picker._PNImagePickerDelegate.imagePickerController_didFinishPickingMediaWithInfo_"
125
- )
126
-
127
- @objc_method
128
- def imagePickerControllerDidCancel_(self, picker: Any) -> None:
129
- try:
130
- picker.dismissViewControllerAnimated_completion_(True, None)
131
- except Exception:
132
- diagnostics.swallowed(
133
- "camera._ios_launch_picker._PNImagePickerDelegate.imagePickerControllerDidCancel_"
134
- )
135
- cb = self._callback
136
- self._callback = None
137
- _pending_delegates.pop(id(self), None)
138
- if cb is not None:
139
- try:
140
- cb(None)
141
- except Exception:
142
- diagnostics.swallowed(
143
- "camera._ios_launch_picker._PNImagePickerDelegate.imagePickerControllerDidCancel_"
144
- )
145
-
146
- delegate = _PNImagePickerDelegate.new()
147
- delegate._callback = on_result
148
- _pending_delegates[id(delegate)] = delegate
149
-
150
- picker = UIImagePickerController.alloc().init()
151
- picker.setSourceType_(1 if source == "camera" else 0)
152
- picker.setDelegate_(delegate)
153
-
154
- UIApplication = ObjCClass("UIApplication")
155
- top = UIApplication.sharedApplication.keyWindow.rootViewController
156
- while top is not None and top.presentedViewController is not None:
157
- top = top.presentedViewController
158
- if top is not None:
159
- top.presentViewController_animated_completion_(picker, True, None)
160
- else:
161
- _pending_delegates.pop(id(delegate), None)
162
- on_result(None)
163
-
164
- # Reference SEL/objc_method so the lint pass keeps the import;
165
- # they're needed for the delegate class above.
166
- _ = (SEL, objc_method)
167
- except Exception:
168
- on_result(None)
169
-
170
-
171
- def _ios_extract_path(info: Any) -> Optional[str]:
172
- """Best-effort extraction of an image path from picker ``info``."""
173
- try:
174
- url = info.objectForKey_("UIImagePickerControllerImageURL")
175
- if url is not None:
176
- try:
177
- return str(url.absoluteString)
178
- except Exception:
179
- try:
180
- return str(url.path)
181
- except Exception:
182
- diagnostics.swallowed("camera._ios_extract_path")
183
- image = info.objectForKey_("UIImagePickerControllerOriginalImage")
184
- if image is not None:
185
- return _ios_write_image_to_tmp(image)
186
- except Exception:
187
- return None
188
- return None
189
-
190
-
191
- def _ios_write_image_to_tmp(image: Any) -> Optional[str]:
192
- """Encode a UIImage to JPEG and write it to NSCachesDirectory."""
193
- try:
194
- from rubicon.objc import ObjCClass
195
-
196
- try:
197
- data = image.jpegDataWithCompressionQuality_(0.85)
198
- except Exception:
199
- return None
200
- if data is None:
201
- return None
202
- NSString = ObjCClass("NSString")
203
- NSFileManager = ObjCClass("NSFileManager")
204
- manager = NSFileManager.defaultManager
205
- urls = manager.URLsForDirectory_inDomains_(13, 1) # NSCachesDirectory, NSUserDomainMask
206
- if urls is None or urls.count == 0:
207
- return None
208
- cache_dir = urls.firstObject
209
- import time as _time
210
-
211
- filename = NSString.stringWithFormat_("pn-camera-%d.jpg", int(_time.time() * 1000))
212
- target = cache_dir.URLByAppendingPathComponent_(filename)
213
- if not data.writeToURL_atomically_(target, True):
214
- return None
215
- try:
216
- return str(target.path)
217
- except Exception:
218
- return str(target.absoluteString)
219
- except Exception:
220
- return None
221
-
222
-
223
- # ======================================================================
224
- # Android implementation: ActivityResultLauncher / startActivityForResult
225
- # ======================================================================
226
-
227
-
228
- _android_pending_results: Dict[int, Callable[[Optional[str]], None]] = {}
229
- _android_next_request_code: int = 50001
230
-
231
-
232
- def _android_next_code() -> int:
233
- global _android_next_request_code
234
- code = _android_next_request_code
235
- _android_next_request_code += 1
236
- return code
237
-
238
-
239
- def _android_launch_picker(on_result: Callable[[Optional[str]], None], source: str) -> None:
240
- try:
241
- from java import jclass
242
-
243
- from ..utils import get_android_context
53
+ return await _launch("pick_from_gallery", options)
244
54
 
245
- Intent = jclass("android.content.Intent")
246
- MediaStore = jclass("android.provider.MediaStore")
247
- ctx = get_android_context()
248
55
 
249
- if source == "camera":
250
- intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
251
- else:
252
- intent = Intent(Intent.ACTION_PICK)
253
- intent.setType("image/*")
254
-
255
- # ``startActivityForResult`` requires the host to be an
256
- # Activity; in unit tests we may only have an Application
257
- # context, in which case we fall back to ``startActivity`` and
258
- # report ``None`` (the caller has no Activity to dispatch the
259
- # result back through).
260
- Activity = jclass("android.app.Activity")
261
- if not Activity.isInstance(ctx):
262
- ctx.startActivity(intent)
263
- on_result(None)
264
- return
265
-
266
- request_code = _android_next_code()
267
- _android_pending_results[request_code] = on_result
268
- try:
269
- ctx.startActivityForResult(intent, request_code)
270
- except Exception:
271
- _android_pending_results.pop(request_code, None)
272
- on_result(None)
273
- except Exception:
274
- on_result(None)
275
-
276
-
277
- def deliver_android_activity_result(request_code: int, result_code: int, data: Any) -> bool:
278
- """Forward an Activity result to the registered camera coroutine.
279
-
280
- The host Activity should call this from ``onActivityResult`` so
281
- the pending
282
- [`take_photo`][pythonnative.native_modules.camera.Camera.take_photo]
283
- /
284
- [`pick_from_gallery`][pythonnative.native_modules.camera.Camera.pick_from_gallery]
285
- awaitable receives a path. Returns ``True`` if a Python callback
286
- was invoked (so the host can short-circuit further handlers).
287
- """
288
- cb = _android_pending_results.pop(request_code, None)
289
- if cb is None:
290
- return False
291
- path: Optional[str] = None
292
- try:
293
- if result_code == -1 and data is not None: # RESULT_OK
294
- uri = data.getData()
295
- if uri is not None:
296
- path = str(uri)
297
- else:
298
- try:
299
- extras = data.getExtras()
300
- if extras is not None:
301
- thumb = extras.get("data")
302
- if thumb is not None:
303
- path = _android_write_bitmap_to_cache(thumb)
304
- except Exception:
305
- diagnostics.swallowed("camera.deliver_android_activity_result")
306
- except Exception:
307
- path = None
56
+ async def _launch(method: str, options: Any) -> Optional[str]:
308
57
  try:
309
- cb(path)
310
- except Exception:
311
- diagnostics.swallowed("camera.deliver_android_activity_result")
312
- return True
313
-
314
-
315
- def _android_write_bitmap_to_cache(bitmap: Any) -> Optional[str]:
316
- """Persist a Bitmap to the app cache directory and return its path."""
317
- try:
318
- from java import jclass
319
-
320
- from ..utils import get_android_context
321
-
322
- ctx = get_android_context()
323
- cache_dir = ctx.getCacheDir()
324
- File = jclass("java.io.File")
325
- FileOutputStream = jclass("java.io.FileOutputStream")
326
- Bitmap = jclass("android.graphics.Bitmap")
327
- import time as _time
328
-
329
- target = File(cache_dir, f"pn-camera-{int(_time.time() * 1000)}.jpg")
330
- out = FileOutputStream(target)
331
- try:
332
- bitmap.compress(Bitmap.CompressFormat.JPEG, 85, out)
333
- finally:
334
- out.close()
335
- return str(target.getAbsolutePath())
58
+ result = await native_module("Camera").call_async(method, **options)
336
59
  except Exception:
337
60
  return None
61
+ return str(result) if result else None