pyxpo-device 0.1.0a1__tar.gz
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.
- pyxpo_device-0.1.0a1/LICENSE +21 -0
- pyxpo_device-0.1.0a1/PKG-INFO +80 -0
- pyxpo_device-0.1.0a1/README.md +61 -0
- pyxpo_device-0.1.0a1/pyproject.toml +42 -0
- pyxpo_device-0.1.0a1/pyxpo_device/__init__.py +163 -0
- pyxpo_device-0.1.0a1/pyxpo_device/android/src/main/AndroidManifest.xml +1 -0
- pyxpo_device-0.1.0a1/pyxpo_device/android/src/main/java/dev/pyxpo/modules/device/DeviceModule.kt +91 -0
- pyxpo_device-0.1.0a1/pyxpo_device/ios/DeviceModule.swift +120 -0
- pyxpo_device-0.1.0a1/pyxpo_device/py.typed +0 -0
- pyxpo_device-0.1.0a1/pyxpo_device/pyxpo-module.json +15 -0
- pyxpo_device-0.1.0a1/pyxpo_device.egg-info/PKG-INFO +80 -0
- pyxpo_device-0.1.0a1/pyxpo_device.egg-info/SOURCES.txt +15 -0
- pyxpo_device-0.1.0a1/pyxpo_device.egg-info/dependency_links.txt +1 -0
- pyxpo_device-0.1.0a1/pyxpo_device.egg-info/entry_points.txt +2 -0
- pyxpo_device-0.1.0a1/pyxpo_device.egg-info/requires.txt +1 -0
- pyxpo_device-0.1.0a1/pyxpo_device.egg-info/top_level.txt +1 -0
- pyxpo_device-0.1.0a1/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vishal Chaubey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyxpo-device
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Device facts for pyxpo apps: brand, model, OS, memory, device type (expo-device shape).
|
|
5
|
+
Author: Vishal Chaubey
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pyxpo/pyxpo/tree/main/packages/device
|
|
8
|
+
Project-URL: Issues, https://github.com/pyxpo/pyxpo/issues
|
|
9
|
+
Keywords: pyxpo,pyxpo-module,mobile,android,ios
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: pyxpo<0.2,>=0.1.0a2
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# pyxpo-device
|
|
21
|
+
|
|
22
|
+
Facts about the device a [pyxpo](https://github.com/pyxpo/pyxpo) app runs on: brand, model, OS, memory and
|
|
23
|
+
device type. The API follows [expo-device](https://docs.expo.dev/versions/latest/sdk/device/).
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pip install pyxpo-device
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```toml
|
|
30
|
+
[tool.pyxpo]
|
|
31
|
+
modules = ["device"]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import pyxpo_device as Device
|
|
36
|
+
|
|
37
|
+
print(Device.brand, Device.model_name, Device.os_name, Device.os_version)
|
|
38
|
+
if Device.device_type == Device.DeviceType.TABLET:
|
|
39
|
+
...
|
|
40
|
+
uptime_ms = await Device.get_uptime_async()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Every name also has its expo-device spelling (`Device.modelName`, `Device.getUptimeAsync`).
|
|
44
|
+
|
|
45
|
+
## Constants
|
|
46
|
+
|
|
47
|
+
Read once at app start, no bridge round trip. `None` where a platform has no value.
|
|
48
|
+
|
|
49
|
+
| Name | iOS | Android |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `is_device` | False on the Simulator | False on an emulator (best-effort check) |
|
|
52
|
+
| `brand`, `manufacturer` | `"Apple"` | `Build.BRAND`, `Build.MANUFACTURER` |
|
|
53
|
+
| `model_name` | Marketing name for known iPhones, else the identifier | `Build.MODEL` |
|
|
54
|
+
| `model_id` | Identifier, for example `"iPhone17,3"` | None |
|
|
55
|
+
| `design_name`, `product_name` | None | `Build.DEVICE`, `Build.PRODUCT` |
|
|
56
|
+
| `device_year_class` | Release year of known iPhones | None |
|
|
57
|
+
| `total_memory` | Bytes of RAM | Bytes of RAM |
|
|
58
|
+
| `supported_cpu_architectures` | `["arm64"]` | `Build.SUPPORTED_ABIS` |
|
|
59
|
+
| `os_name` | `"iOS"` or `"iPadOS"` | `"Android"` |
|
|
60
|
+
| `os_version` | For example `"26.0"` | For example `"15"` |
|
|
61
|
+
| `os_build_id`, `os_internal_build_id` | Kernel OS build | `Build.DISPLAY`, `Build.ID` |
|
|
62
|
+
| `os_build_fingerprint`, `platform_api_level` | None | `Build.FINGERPRINT`, API level |
|
|
63
|
+
| `device_name` | `UIDevice.name` (a generic name on iOS 16+) | Device name setting, else the model |
|
|
64
|
+
| `device_type` | `DeviceType` from the interface idiom | `DeviceType` from TV mode and screen width (600dp+ = tablet) |
|
|
65
|
+
|
|
66
|
+
On the desktop the values come from Python's `platform` module and `device_type` is `DESKTOP`.
|
|
67
|
+
|
|
68
|
+
## Functions
|
|
69
|
+
|
|
70
|
+
- `get_device_type_async()` -> `DeviceType`
|
|
71
|
+
- `get_uptime_async()` -> milliseconds since boot, not counting deep sleep
|
|
72
|
+
- `get_max_memory_async()` -> Android: the runtime's memory limit in bytes; elsewhere `MAX_SAFE_INTEGER`
|
|
73
|
+
- `is_rooted_experimental_async()` -> best-effort jailbreak or root check. Easy to fool; never use it for security.
|
|
74
|
+
- `is_side_loading_enabled_async()` -> Android: whether the app may install apps (needs `REQUEST_INSTALL_PACKAGES` on Android 8+)
|
|
75
|
+
- `get_platform_features_async()` -> Android system feature names; `[]` elsewhere
|
|
76
|
+
- `has_platform_feature_async(feature)` -> Android: for example `"android.hardware.camera"`; `False` elsewhere
|
|
77
|
+
|
|
78
|
+
## Permissions
|
|
79
|
+
|
|
80
|
+
None.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# pyxpo-device
|
|
2
|
+
|
|
3
|
+
Facts about the device a [pyxpo](https://github.com/pyxpo/pyxpo) app runs on: brand, model, OS, memory and
|
|
4
|
+
device type. The API follows [expo-device](https://docs.expo.dev/versions/latest/sdk/device/).
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
pip install pyxpo-device
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```toml
|
|
11
|
+
[tool.pyxpo]
|
|
12
|
+
modules = ["device"]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import pyxpo_device as Device
|
|
17
|
+
|
|
18
|
+
print(Device.brand, Device.model_name, Device.os_name, Device.os_version)
|
|
19
|
+
if Device.device_type == Device.DeviceType.TABLET:
|
|
20
|
+
...
|
|
21
|
+
uptime_ms = await Device.get_uptime_async()
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Every name also has its expo-device spelling (`Device.modelName`, `Device.getUptimeAsync`).
|
|
25
|
+
|
|
26
|
+
## Constants
|
|
27
|
+
|
|
28
|
+
Read once at app start, no bridge round trip. `None` where a platform has no value.
|
|
29
|
+
|
|
30
|
+
| Name | iOS | Android |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `is_device` | False on the Simulator | False on an emulator (best-effort check) |
|
|
33
|
+
| `brand`, `manufacturer` | `"Apple"` | `Build.BRAND`, `Build.MANUFACTURER` |
|
|
34
|
+
| `model_name` | Marketing name for known iPhones, else the identifier | `Build.MODEL` |
|
|
35
|
+
| `model_id` | Identifier, for example `"iPhone17,3"` | None |
|
|
36
|
+
| `design_name`, `product_name` | None | `Build.DEVICE`, `Build.PRODUCT` |
|
|
37
|
+
| `device_year_class` | Release year of known iPhones | None |
|
|
38
|
+
| `total_memory` | Bytes of RAM | Bytes of RAM |
|
|
39
|
+
| `supported_cpu_architectures` | `["arm64"]` | `Build.SUPPORTED_ABIS` |
|
|
40
|
+
| `os_name` | `"iOS"` or `"iPadOS"` | `"Android"` |
|
|
41
|
+
| `os_version` | For example `"26.0"` | For example `"15"` |
|
|
42
|
+
| `os_build_id`, `os_internal_build_id` | Kernel OS build | `Build.DISPLAY`, `Build.ID` |
|
|
43
|
+
| `os_build_fingerprint`, `platform_api_level` | None | `Build.FINGERPRINT`, API level |
|
|
44
|
+
| `device_name` | `UIDevice.name` (a generic name on iOS 16+) | Device name setting, else the model |
|
|
45
|
+
| `device_type` | `DeviceType` from the interface idiom | `DeviceType` from TV mode and screen width (600dp+ = tablet) |
|
|
46
|
+
|
|
47
|
+
On the desktop the values come from Python's `platform` module and `device_type` is `DESKTOP`.
|
|
48
|
+
|
|
49
|
+
## Functions
|
|
50
|
+
|
|
51
|
+
- `get_device_type_async()` -> `DeviceType`
|
|
52
|
+
- `get_uptime_async()` -> milliseconds since boot, not counting deep sleep
|
|
53
|
+
- `get_max_memory_async()` -> Android: the runtime's memory limit in bytes; elsewhere `MAX_SAFE_INTEGER`
|
|
54
|
+
- `is_rooted_experimental_async()` -> best-effort jailbreak or root check. Easy to fool; never use it for security.
|
|
55
|
+
- `is_side_loading_enabled_async()` -> Android: whether the app may install apps (needs `REQUEST_INSTALL_PACKAGES` on Android 8+)
|
|
56
|
+
- `get_platform_features_async()` -> Android system feature names; `[]` elsewhere
|
|
57
|
+
- `has_platform_feature_async(feature)` -> Android: for example `"android.hardware.camera"`; `False` elsewhere
|
|
58
|
+
|
|
59
|
+
## Permissions
|
|
60
|
+
|
|
61
|
+
None.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyxpo-device"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Device facts for pyxpo apps: brand, model, OS, memory, device type (expo-device shape)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Vishal Chaubey" }]
|
|
13
|
+
keywords = ["pyxpo", "pyxpo-module", "mobile", "android", "ios"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Software Development :: User Interfaces",
|
|
19
|
+
]
|
|
20
|
+
requires-python = ">=3.9"
|
|
21
|
+
dependencies = ["pyxpo>=0.1.0a2,<0.2"]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/pyxpo/pyxpo/tree/main/packages/device"
|
|
25
|
+
Issues = "https://github.com/pyxpo/pyxpo/issues"
|
|
26
|
+
|
|
27
|
+
# How `pyxpo build` finds this module: the entry point name is the module name apps list in
|
|
28
|
+
# `[tool.pyxpo] modules`, its value the import package holding pyxpo-module.json.
|
|
29
|
+
[project.entry-points."pyxpo.modules"]
|
|
30
|
+
device = "pyxpo_device"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
packages = ["pyxpo_device"]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.dynamic]
|
|
36
|
+
version = { attr = "pyxpo_device.__version__" }
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-data]
|
|
39
|
+
pyxpo_device = ["pyxpo-module.json", "py.typed", "ios/**/*", "android/**/*"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.exclude-package-data]
|
|
42
|
+
"*" = ["__pycache__", "*.pyc", ".DS_Store"]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Facts about the device the app runs on: the expo-device API.
|
|
2
|
+
|
|
3
|
+
import pyxpo_device as Device
|
|
4
|
+
|
|
5
|
+
Device.brand, Device.model_name, Device.os_name, Device.os_version # or Device.modelName, Device.osName ...
|
|
6
|
+
if Device.device_type == Device.DeviceType.TABLET: ...
|
|
7
|
+
ms = await Device.get_uptime_async()
|
|
8
|
+
|
|
9
|
+
The constants are read once at startup (no bridge round trip). Values a platform does not have are None:
|
|
10
|
+
`model_id` is iOS only; `design_name`, `product_name`, `os_build_fingerprint` and `platform_api_level` are
|
|
11
|
+
Android only. `device_year_class` is the release year of the iPhone model (None for unknown models and on
|
|
12
|
+
Android). On the desktop the values come from Python's `platform` module.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import platform as _py_platform
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
from enum import IntEnum
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
from pyxpo.module_api import call, constants, platform, require_linked
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0a1"
|
|
26
|
+
|
|
27
|
+
# Number.MAX_SAFE_INTEGER: what expo-device returns when memory has no inherent limit.
|
|
28
|
+
MAX_SAFE_INTEGER = 2 ** 53 - 1
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DeviceType(IntEnum):
|
|
32
|
+
"""Kind of device, from the screen size and the platform's own idiom."""
|
|
33
|
+
UNKNOWN = 0
|
|
34
|
+
PHONE = 1
|
|
35
|
+
TABLET = 2
|
|
36
|
+
DESKTOP = 3
|
|
37
|
+
TV = 4
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_FIELDS = {
|
|
41
|
+
"isDevice": "is_device", "brand": "brand", "manufacturer": "manufacturer", "modelName": "model_name",
|
|
42
|
+
"modelId": "model_id", "designName": "design_name", "productName": "product_name",
|
|
43
|
+
"deviceYearClass": "device_year_class", "totalMemory": "total_memory",
|
|
44
|
+
"supportedCpuArchitectures": "supported_cpu_architectures", "osName": "os_name", "osVersion": "os_version",
|
|
45
|
+
"osBuildId": "os_build_id", "osInternalBuildId": "os_internal_build_id",
|
|
46
|
+
"osBuildFingerprint": "os_build_fingerprint", "platformApiLevel": "platform_api_level",
|
|
47
|
+
"deviceName": "device_name", "deviceType": "device_type",
|
|
48
|
+
}
|
|
49
|
+
_BY_SNAKE = {snake: camel for camel, snake in _FIELDS.items()}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _native() -> bool:
|
|
53
|
+
if platform() == "desktop":
|
|
54
|
+
return False
|
|
55
|
+
require_linked("Device", "device", package="pyxpo-device")
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _desktop() -> Dict[str, Any]:
|
|
60
|
+
u = _py_platform.uname()
|
|
61
|
+
total: Optional[int] = None
|
|
62
|
+
try:
|
|
63
|
+
total = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
|
|
64
|
+
except (AttributeError, ValueError, OSError):
|
|
65
|
+
pass
|
|
66
|
+
return {
|
|
67
|
+
"isDevice": True, "brand": None, "manufacturer": None, "modelName": None, "modelId": None,
|
|
68
|
+
"designName": None, "productName": None, "deviceYearClass": None, "totalMemory": total,
|
|
69
|
+
"supportedCpuArchitectures": [u.machine] if u.machine else None, "osName": u.system or sys.platform,
|
|
70
|
+
"osVersion": u.release or None, "osBuildId": u.version or None, "osInternalBuildId": u.version or None,
|
|
71
|
+
"osBuildFingerprint": None, "platformApiLevel": None, "deviceName": u.node or None,
|
|
72
|
+
"deviceType": int(DeviceType.DESKTOP),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _all() -> Dict[str, Any]:
|
|
77
|
+
if not _native():
|
|
78
|
+
return _desktop()
|
|
79
|
+
c = constants("Device")
|
|
80
|
+
return {k: c.get(k) for k in _FIELDS}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def __getattr__(name: str) -> Any:
|
|
84
|
+
# Constants resolve on access: the startup env is not there yet when this module is imported.
|
|
85
|
+
camel = name if name in _FIELDS else _BY_SNAKE.get(name)
|
|
86
|
+
if camel is None:
|
|
87
|
+
raise AttributeError("module 'pyxpo_device' has no attribute %r" % name)
|
|
88
|
+
value = _all()[camel]
|
|
89
|
+
if camel == "deviceType":
|
|
90
|
+
return DeviceType(value) if value in DeviceType._value2member_map_ else DeviceType.UNKNOWN
|
|
91
|
+
if camel == "supportedCpuArchitectures" and value is not None:
|
|
92
|
+
return list(value)
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def get_device_type_async() -> DeviceType:
|
|
97
|
+
"""The `DeviceType` of this device (same as `device_type`)."""
|
|
98
|
+
return __getattr__("deviceType")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def get_uptime_async() -> int:
|
|
102
|
+
"""Milliseconds since the device booted, not counting deep sleep."""
|
|
103
|
+
if not _native():
|
|
104
|
+
return int(time.monotonic() * 1000) # the system clock since boot on macOS and Linux
|
|
105
|
+
return int(await call("Device", "getUptime", reply=True))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def get_max_memory_async() -> int:
|
|
109
|
+
"""Most memory (bytes) the Android runtime lets the app use. `MAX_SAFE_INTEGER` where there is no limit
|
|
110
|
+
(iOS, the desktop)."""
|
|
111
|
+
if platform() != "android" or not _native():
|
|
112
|
+
return MAX_SAFE_INTEGER
|
|
113
|
+
return int(await call("Device", "getMaxMemory", reply=True))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def is_rooted_experimental_async() -> bool:
|
|
117
|
+
"""Best-effort jailbreak (iOS) or root (Android) check. Easy to fool: never use it for security."""
|
|
118
|
+
if not _native():
|
|
119
|
+
return False
|
|
120
|
+
return bool(await call("Device", "isRooted", reply=True))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def is_side_loading_enabled_async() -> bool:
|
|
124
|
+
"""Android: whether this app may install other apps (needs REQUEST_INSTALL_PACKAGES on Android 8+).
|
|
125
|
+
Always False on iOS and the desktop."""
|
|
126
|
+
if platform() != "android" or not _native():
|
|
127
|
+
return False
|
|
128
|
+
return bool(await call("Device", "isSideLoadingEnabled", reply=True))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def get_platform_features_async() -> List[str]:
|
|
132
|
+
"""Android: the system feature names the device reports (`PackageManager.getSystemAvailableFeatures`).
|
|
133
|
+
Empty on iOS and the desktop."""
|
|
134
|
+
if platform() != "android" or not _native():
|
|
135
|
+
return []
|
|
136
|
+
return list(await call("Device", "getPlatformFeatures", reply=True) or [])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def has_platform_feature_async(feature: str) -> bool:
|
|
140
|
+
"""Android: whether the device has a system feature, for example "android.hardware.camera".
|
|
141
|
+
Always False on iOS and the desktop."""
|
|
142
|
+
if not isinstance(feature, str) or not feature:
|
|
143
|
+
raise ValueError("feature must be a non-empty string")
|
|
144
|
+
if platform() != "android" or not _native():
|
|
145
|
+
return False
|
|
146
|
+
return bool(await call("Device", "hasPlatformFeature", feature, reply=True))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
getDeviceTypeAsync = get_device_type_async
|
|
150
|
+
getUptimeAsync = get_uptime_async
|
|
151
|
+
getMaxMemoryAsync = get_max_memory_async
|
|
152
|
+
isRootedExperimentalAsync = is_rooted_experimental_async
|
|
153
|
+
isSideLoadingEnabledAsync = is_side_loading_enabled_async
|
|
154
|
+
getPlatformFeaturesAsync = get_platform_features_async
|
|
155
|
+
hasPlatformFeatureAsync = has_platform_feature_async
|
|
156
|
+
|
|
157
|
+
__all__ = [
|
|
158
|
+
"DeviceType", "MAX_SAFE_INTEGER", "get_device_type_async", "get_uptime_async", "get_max_memory_async",
|
|
159
|
+
"is_rooted_experimental_async", "is_side_loading_enabled_async", "get_platform_features_async",
|
|
160
|
+
"has_platform_feature_async", "getDeviceTypeAsync", "getUptimeAsync", "getMaxMemoryAsync",
|
|
161
|
+
"isRootedExperimentalAsync", "isSideLoadingEnabledAsync", "getPlatformFeaturesAsync",
|
|
162
|
+
"hasPlatformFeatureAsync",
|
|
163
|
+
] + list(_FIELDS) + list(_FIELDS.values())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
pyxpo_device-0.1.0a1/pyxpo_device/android/src/main/java/dev/pyxpo/modules/device/DeviceModule.kt
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
package dev.pyxpo.modules.device
|
|
2
|
+
|
|
3
|
+
import android.app.ActivityManager
|
|
4
|
+
import android.app.UiModeManager
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.res.Configuration
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.os.SystemClock
|
|
9
|
+
import android.provider.Settings
|
|
10
|
+
import dev.pyxpo.moduleapi.PxModule
|
|
11
|
+
import dev.pyxpo.moduleapi.PxModuleContext
|
|
12
|
+
import dev.pyxpo.moduleapi.PxReply
|
|
13
|
+
import org.json.JSONArray
|
|
14
|
+
import java.io.File
|
|
15
|
+
|
|
16
|
+
/** expo-device on Android. Constants come from `Build`, `ActivityManager` and the configuration at startup. */
|
|
17
|
+
class DeviceModule : PxModule() {
|
|
18
|
+
override val name = "Device"
|
|
19
|
+
|
|
20
|
+
override fun constants(context: Context): Map<String, Any?> {
|
|
21
|
+
val mem = ActivityManager.MemoryInfo()
|
|
22
|
+
(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(mem)
|
|
23
|
+
return mapOf(
|
|
24
|
+
"isDevice" to !isEmulator(),
|
|
25
|
+
"brand" to Build.BRAND,
|
|
26
|
+
"manufacturer" to Build.MANUFACTURER,
|
|
27
|
+
"modelName" to Build.MODEL,
|
|
28
|
+
"modelId" to null,
|
|
29
|
+
"designName" to Build.DEVICE,
|
|
30
|
+
"productName" to Build.PRODUCT,
|
|
31
|
+
"deviceYearClass" to null,
|
|
32
|
+
"totalMemory" to mem.totalMem,
|
|
33
|
+
"supportedCpuArchitectures" to Build.SUPPORTED_ABIS.toList(),
|
|
34
|
+
"osName" to "Android",
|
|
35
|
+
"osVersion" to Build.VERSION.RELEASE,
|
|
36
|
+
"osBuildId" to Build.DISPLAY,
|
|
37
|
+
"osInternalBuildId" to Build.ID,
|
|
38
|
+
"osBuildFingerprint" to Build.FINGERPRINT,
|
|
39
|
+
"platformApiLevel" to Build.VERSION.SDK_INT,
|
|
40
|
+
"deviceName" to (Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME) ?: Build.MODEL),
|
|
41
|
+
"deviceType" to deviceType(context),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private fun isEmulator(): Boolean =
|
|
46
|
+
Build.FINGERPRINT.startsWith("generic") || Build.FINGERPRINT.contains("emulator") ||
|
|
47
|
+
Build.HARDWARE == "goldfish" || Build.HARDWARE == "ranchu" ||
|
|
48
|
+
Build.MODEL.contains("Emulator") || Build.MODEL.contains("Android SDK built for") ||
|
|
49
|
+
Build.PRODUCT.startsWith("sdk") || Build.PRODUCT.contains("emulator") || Build.PRODUCT.contains("simulator")
|
|
50
|
+
|
|
51
|
+
private fun deviceType(context: Context): Int {
|
|
52
|
+
val ui = context.getSystemService(Context.UI_MODE_SERVICE) as? UiModeManager
|
|
53
|
+
if (ui?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) return 4
|
|
54
|
+
val pm = context.packageManager
|
|
55
|
+
if (pm.hasSystemFeature("android.software.leanback")) return 4
|
|
56
|
+
if (pm.hasSystemFeature("org.chromium.arc") || pm.hasSystemFeature("org.chromium.arc.device_management")) return 3
|
|
57
|
+
return if (context.resources.configuration.smallestScreenWidthDp >= 600) 2 else 1
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private fun isRooted(): Boolean {
|
|
61
|
+
if (Build.TAGS?.contains("test-keys") == true) return true
|
|
62
|
+
val paths = listOf("/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su",
|
|
63
|
+
"/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su", "/system/bin/failsafe/su", "/data/local/su")
|
|
64
|
+
return paths.any { File(it).exists() }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Android 8+ throws unless the app declares REQUEST_INSTALL_PACKAGES; without it the answer is false.
|
|
68
|
+
private fun sideLoading(c: Context): Boolean =
|
|
69
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
70
|
+
try { c.packageManager.canRequestPackageInstalls() } catch (e: SecurityException) { false }
|
|
71
|
+
} else {
|
|
72
|
+
@Suppress("DEPRECATION")
|
|
73
|
+
Settings.Secure.getInt(c.contentResolver, Settings.Secure.INSTALL_NON_MARKET_APPS, 0) == 1
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
override fun call(ctx: PxModuleContext, method: String, args: JSONArray, reply: PxReply) {
|
|
77
|
+
val c = ctx.activity
|
|
78
|
+
when (method) {
|
|
79
|
+
"getUptime" -> reply(SystemClock.uptimeMillis(), null)
|
|
80
|
+
"getMaxMemory" -> {
|
|
81
|
+
val max = Runtime.getRuntime().maxMemory()
|
|
82
|
+
reply(if (max == Long.MAX_VALUE) 9007199254740991L else max, null)
|
|
83
|
+
}
|
|
84
|
+
"isRooted" -> reply(isRooted(), null)
|
|
85
|
+
"isSideLoadingEnabled" -> reply(sideLoading(c), null)
|
|
86
|
+
"getPlatformFeatures" -> reply(c.packageManager.systemAvailableFeatures.mapNotNull { it.name }, null)
|
|
87
|
+
"hasPlatformFeature" -> reply(c.packageManager.hasSystemFeature(args.optString(0)), null)
|
|
88
|
+
else -> reply(null, "Device has no method '$method'")
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// expo-device on iOS. Constants are read once at startup; the model name comes from a table of iPhone
|
|
4
|
+
/// identifiers (unknown models, iPads and newer phones report the raw identifier, for example "iPad14,1").
|
|
5
|
+
final class DeviceModule: PxModule {
|
|
6
|
+
static let moduleName = "Device"
|
|
7
|
+
|
|
8
|
+
required init() {}
|
|
9
|
+
|
|
10
|
+
/// iPhone identifier: (marketing name, release year).
|
|
11
|
+
private static let iPhones: [String: (String, Int)] = [
|
|
12
|
+
"iPhone10,1": ("iPhone 8", 2017), "iPhone10,4": ("iPhone 8", 2017),
|
|
13
|
+
"iPhone10,2": ("iPhone 8 Plus", 2017), "iPhone10,5": ("iPhone 8 Plus", 2017),
|
|
14
|
+
"iPhone10,3": ("iPhone X", 2017), "iPhone10,6": ("iPhone X", 2017),
|
|
15
|
+
"iPhone11,2": ("iPhone XS", 2018), "iPhone11,4": ("iPhone XS Max", 2018),
|
|
16
|
+
"iPhone11,6": ("iPhone XS Max", 2018), "iPhone11,8": ("iPhone XR", 2018),
|
|
17
|
+
"iPhone12,1": ("iPhone 11", 2019), "iPhone12,3": ("iPhone 11 Pro", 2019),
|
|
18
|
+
"iPhone12,5": ("iPhone 11 Pro Max", 2019), "iPhone12,8": ("iPhone SE (2nd generation)", 2020),
|
|
19
|
+
"iPhone13,1": ("iPhone 12 mini", 2020), "iPhone13,2": ("iPhone 12", 2020),
|
|
20
|
+
"iPhone13,3": ("iPhone 12 Pro", 2020), "iPhone13,4": ("iPhone 12 Pro Max", 2020),
|
|
21
|
+
"iPhone14,4": ("iPhone 13 mini", 2021), "iPhone14,5": ("iPhone 13", 2021),
|
|
22
|
+
"iPhone14,2": ("iPhone 13 Pro", 2021), "iPhone14,3": ("iPhone 13 Pro Max", 2021),
|
|
23
|
+
"iPhone14,6": ("iPhone SE (3rd generation)", 2022), "iPhone14,7": ("iPhone 14", 2022),
|
|
24
|
+
"iPhone14,8": ("iPhone 14 Plus", 2022), "iPhone15,2": ("iPhone 14 Pro", 2022),
|
|
25
|
+
"iPhone15,3": ("iPhone 14 Pro Max", 2022), "iPhone15,4": ("iPhone 15", 2023),
|
|
26
|
+
"iPhone15,5": ("iPhone 15 Plus", 2023), "iPhone16,1": ("iPhone 15 Pro", 2023),
|
|
27
|
+
"iPhone16,2": ("iPhone 15 Pro Max", 2023), "iPhone17,3": ("iPhone 16", 2024),
|
|
28
|
+
"iPhone17,4": ("iPhone 16 Plus", 2024), "iPhone17,1": ("iPhone 16 Pro", 2024),
|
|
29
|
+
"iPhone17,2": ("iPhone 16 Pro Max", 2024), "iPhone17,5": ("iPhone 16e", 2025),
|
|
30
|
+
"iPhone18,3": ("iPhone 17", 2025), "iPhone18,1": ("iPhone 17 Pro", 2025),
|
|
31
|
+
"iPhone18,2": ("iPhone 17 Pro Max", 2025), "iPhone18,4": ("iPhone Air", 2025),
|
|
32
|
+
"iPhone18,5": ("iPhone 17e", 2026), "iPhone19,2": ("iPhone 18 Pro", 2026),
|
|
33
|
+
"iPhone19,3": ("iPhone 18 Pro Max", 2026),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
private static func sysctlString(_ name: String) -> String? {
|
|
37
|
+
var size = 0
|
|
38
|
+
guard sysctlbyname(name, nil, &size, nil, 0) == 0, size > 0 else { return nil }
|
|
39
|
+
var buf = [CChar](repeating: 0, count: size)
|
|
40
|
+
guard sysctlbyname(name, &buf, &size, nil, 0) == 0 else { return nil }
|
|
41
|
+
return String(cString: buf)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private static var isSimulator: Bool {
|
|
45
|
+
#if targetEnvironment(simulator)
|
|
46
|
+
return true
|
|
47
|
+
#else
|
|
48
|
+
return false
|
|
49
|
+
#endif
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private static var modelId: String? {
|
|
53
|
+
if isSimulator, let id = ProcessInfo.processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] { return id }
|
|
54
|
+
return sysctlString("hw.machine")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private static var deviceType: Int {
|
|
58
|
+
if ProcessInfo.processInfo.isiOSAppOnMac || ProcessInfo.processInfo.isMacCatalystApp { return 3 }
|
|
59
|
+
switch UIDevice.current.userInterfaceIdiom {
|
|
60
|
+
case .phone: return 1
|
|
61
|
+
case .pad: return 2
|
|
62
|
+
case .mac: return 3
|
|
63
|
+
case .tv: return 4
|
|
64
|
+
default: return 0
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static var constants: [String: Any] {
|
|
69
|
+
let d = UIDevice.current
|
|
70
|
+
let id = modelId
|
|
71
|
+
let known = id.flatMap { iPhones[$0] }
|
|
72
|
+
let build: Any = sysctlString("kern.osversion") ?? NSNull()
|
|
73
|
+
var arch = "arm64"
|
|
74
|
+
#if arch(x86_64)
|
|
75
|
+
arch = "x86_64"
|
|
76
|
+
#endif
|
|
77
|
+
return [
|
|
78
|
+
"isDevice": !isSimulator,
|
|
79
|
+
"brand": "Apple",
|
|
80
|
+
"manufacturer": "Apple",
|
|
81
|
+
"modelName": known?.0 ?? id ?? NSNull(),
|
|
82
|
+
"modelId": id ?? NSNull(),
|
|
83
|
+
"designName": NSNull(),
|
|
84
|
+
"productName": NSNull(),
|
|
85
|
+
"deviceYearClass": known?.1 ?? NSNull(),
|
|
86
|
+
"totalMemory": ProcessInfo.processInfo.physicalMemory,
|
|
87
|
+
"supportedCpuArchitectures": [arch],
|
|
88
|
+
"osName": d.userInterfaceIdiom == .pad ? "iPadOS" : d.systemName,
|
|
89
|
+
"osVersion": d.systemVersion,
|
|
90
|
+
"osBuildId": build,
|
|
91
|
+
"osInternalBuildId": build,
|
|
92
|
+
"osBuildFingerprint": NSNull(),
|
|
93
|
+
"platformApiLevel": NSNull(),
|
|
94
|
+
"deviceName": d.name,
|
|
95
|
+
"deviceType": deviceType,
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private static var looksJailbroken: Bool {
|
|
100
|
+
if isSimulator { return false }
|
|
101
|
+
let paths = ["/Applications/Cydia.app", "/Library/MobileSubstrate/MobileSubstrate.dylib", "/bin/bash",
|
|
102
|
+
"/usr/sbin/sshd", "/etc/apt", "/private/var/lib/apt/", "/var/jb"]
|
|
103
|
+
if paths.contains(where: { FileManager.default.fileExists(atPath: $0) }) { return true }
|
|
104
|
+
// A sandboxed app cannot write outside its container.
|
|
105
|
+
let probe = "/private/pyxpo-jb-probe.txt"
|
|
106
|
+
if (try? "x".write(toFile: probe, atomically: true, encoding: .utf8)) != nil {
|
|
107
|
+
try? FileManager.default.removeItem(atPath: probe)
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func call(_ ctx: PxModuleContext, _ method: String, _ args: [Any], _ reply: @escaping PxReply) {
|
|
114
|
+
switch method {
|
|
115
|
+
case "getUptime": reply(Int(ProcessInfo.processInfo.systemUptime * 1000), nil)
|
|
116
|
+
case "isRooted": reply(Self.looksJailbroken, nil)
|
|
117
|
+
default: reply(nil, "Device has no method '\(method)'")
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"nativeContract": 2,
|
|
4
|
+
"name": "device",
|
|
5
|
+
"native": "Device",
|
|
6
|
+
"ios": {
|
|
7
|
+
"classes": ["DeviceModule"]
|
|
8
|
+
},
|
|
9
|
+
"android": {
|
|
10
|
+
"namespace": "dev.pyxpo.modules.device",
|
|
11
|
+
"classes": ["dev.pyxpo.modules.device.DeviceModule"],
|
|
12
|
+
"dependencies": [],
|
|
13
|
+
"minSdk": 24
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyxpo-device
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Device facts for pyxpo apps: brand, model, OS, memory, device type (expo-device shape).
|
|
5
|
+
Author: Vishal Chaubey
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pyxpo/pyxpo/tree/main/packages/device
|
|
8
|
+
Project-URL: Issues, https://github.com/pyxpo/pyxpo/issues
|
|
9
|
+
Keywords: pyxpo,pyxpo-module,mobile,android,ios
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: pyxpo<0.2,>=0.1.0a2
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# pyxpo-device
|
|
21
|
+
|
|
22
|
+
Facts about the device a [pyxpo](https://github.com/pyxpo/pyxpo) app runs on: brand, model, OS, memory and
|
|
23
|
+
device type. The API follows [expo-device](https://docs.expo.dev/versions/latest/sdk/device/).
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pip install pyxpo-device
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```toml
|
|
30
|
+
[tool.pyxpo]
|
|
31
|
+
modules = ["device"]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import pyxpo_device as Device
|
|
36
|
+
|
|
37
|
+
print(Device.brand, Device.model_name, Device.os_name, Device.os_version)
|
|
38
|
+
if Device.device_type == Device.DeviceType.TABLET:
|
|
39
|
+
...
|
|
40
|
+
uptime_ms = await Device.get_uptime_async()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Every name also has its expo-device spelling (`Device.modelName`, `Device.getUptimeAsync`).
|
|
44
|
+
|
|
45
|
+
## Constants
|
|
46
|
+
|
|
47
|
+
Read once at app start, no bridge round trip. `None` where a platform has no value.
|
|
48
|
+
|
|
49
|
+
| Name | iOS | Android |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `is_device` | False on the Simulator | False on an emulator (best-effort check) |
|
|
52
|
+
| `brand`, `manufacturer` | `"Apple"` | `Build.BRAND`, `Build.MANUFACTURER` |
|
|
53
|
+
| `model_name` | Marketing name for known iPhones, else the identifier | `Build.MODEL` |
|
|
54
|
+
| `model_id` | Identifier, for example `"iPhone17,3"` | None |
|
|
55
|
+
| `design_name`, `product_name` | None | `Build.DEVICE`, `Build.PRODUCT` |
|
|
56
|
+
| `device_year_class` | Release year of known iPhones | None |
|
|
57
|
+
| `total_memory` | Bytes of RAM | Bytes of RAM |
|
|
58
|
+
| `supported_cpu_architectures` | `["arm64"]` | `Build.SUPPORTED_ABIS` |
|
|
59
|
+
| `os_name` | `"iOS"` or `"iPadOS"` | `"Android"` |
|
|
60
|
+
| `os_version` | For example `"26.0"` | For example `"15"` |
|
|
61
|
+
| `os_build_id`, `os_internal_build_id` | Kernel OS build | `Build.DISPLAY`, `Build.ID` |
|
|
62
|
+
| `os_build_fingerprint`, `platform_api_level` | None | `Build.FINGERPRINT`, API level |
|
|
63
|
+
| `device_name` | `UIDevice.name` (a generic name on iOS 16+) | Device name setting, else the model |
|
|
64
|
+
| `device_type` | `DeviceType` from the interface idiom | `DeviceType` from TV mode and screen width (600dp+ = tablet) |
|
|
65
|
+
|
|
66
|
+
On the desktop the values come from Python's `platform` module and `device_type` is `DESKTOP`.
|
|
67
|
+
|
|
68
|
+
## Functions
|
|
69
|
+
|
|
70
|
+
- `get_device_type_async()` -> `DeviceType`
|
|
71
|
+
- `get_uptime_async()` -> milliseconds since boot, not counting deep sleep
|
|
72
|
+
- `get_max_memory_async()` -> Android: the runtime's memory limit in bytes; elsewhere `MAX_SAFE_INTEGER`
|
|
73
|
+
- `is_rooted_experimental_async()` -> best-effort jailbreak or root check. Easy to fool; never use it for security.
|
|
74
|
+
- `is_side_loading_enabled_async()` -> Android: whether the app may install apps (needs `REQUEST_INSTALL_PACKAGES` on Android 8+)
|
|
75
|
+
- `get_platform_features_async()` -> Android system feature names; `[]` elsewhere
|
|
76
|
+
- `has_platform_feature_async(feature)` -> Android: for example `"android.hardware.camera"`; `False` elsewhere
|
|
77
|
+
|
|
78
|
+
## Permissions
|
|
79
|
+
|
|
80
|
+
None.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
pyxpo_device/__init__.py
|
|
5
|
+
pyxpo_device/py.typed
|
|
6
|
+
pyxpo_device/pyxpo-module.json
|
|
7
|
+
pyxpo_device.egg-info/PKG-INFO
|
|
8
|
+
pyxpo_device.egg-info/SOURCES.txt
|
|
9
|
+
pyxpo_device.egg-info/dependency_links.txt
|
|
10
|
+
pyxpo_device.egg-info/entry_points.txt
|
|
11
|
+
pyxpo_device.egg-info/requires.txt
|
|
12
|
+
pyxpo_device.egg-info/top_level.txt
|
|
13
|
+
pyxpo_device/android/src/main/AndroidManifest.xml
|
|
14
|
+
pyxpo_device/android/src/main/java/dev/pyxpo/modules/device/DeviceModule.kt
|
|
15
|
+
pyxpo_device/ios/DeviceModule.swift
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyxpo<0.2,>=0.1.0a2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyxpo_device
|