pyxpo-application 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_application-0.1.0a1/LICENSE +21 -0
- pyxpo_application-0.1.0a1/PKG-INFO +74 -0
- pyxpo_application-0.1.0a1/README.md +55 -0
- pyxpo_application-0.1.0a1/pyproject.toml +42 -0
- pyxpo_application-0.1.0a1/pyxpo_application/__init__.py +132 -0
- pyxpo_application-0.1.0a1/pyxpo_application/android/src/main/AndroidManifest.xml +1 -0
- pyxpo_application-0.1.0a1/pyxpo_application/android/src/main/java/dev/pyxpo/modules/application/ApplicationModule.kt +79 -0
- pyxpo_application-0.1.0a1/pyxpo_application/ios/ApplicationModule.swift +61 -0
- pyxpo_application-0.1.0a1/pyxpo_application/py.typed +0 -0
- pyxpo_application-0.1.0a1/pyxpo_application/pyxpo-module.json +15 -0
- pyxpo_application-0.1.0a1/pyxpo_application.egg-info/PKG-INFO +74 -0
- pyxpo_application-0.1.0a1/pyxpo_application.egg-info/SOURCES.txt +15 -0
- pyxpo_application-0.1.0a1/pyxpo_application.egg-info/dependency_links.txt +1 -0
- pyxpo_application-0.1.0a1/pyxpo_application.egg-info/entry_points.txt +2 -0
- pyxpo_application-0.1.0a1/pyxpo_application.egg-info/requires.txt +1 -0
- pyxpo_application-0.1.0a1/pyxpo_application.egg-info/top_level.txt +1 -0
- pyxpo_application-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,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyxpo-application
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: App identity for pyxpo apps: name, id, version, build, install time (expo-application shape).
|
|
5
|
+
Author: Vishal Chaubey
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pyxpo/pyxpo/tree/main/packages/application
|
|
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-application
|
|
21
|
+
|
|
22
|
+
The installed app's own identity for [pyxpo](https://github.com/pyxpo/pyxpo) apps: name, id, version,
|
|
23
|
+
build number, install time and signing details. The API follows
|
|
24
|
+
[expo-application](https://docs.expo.dev/versions/latest/sdk/application/).
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pip install pyxpo-application
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```toml
|
|
31
|
+
[tool.pyxpo]
|
|
32
|
+
modules = ["application"]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import pyxpo_application as Application
|
|
37
|
+
|
|
38
|
+
print(Application.application_name, Application.application_id)
|
|
39
|
+
print(Application.native_application_version, Application.native_build_version)
|
|
40
|
+
installed = await Application.get_installation_time_async() # timezone-aware datetime (UTC)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Every name also has its expo-application spelling (`Application.applicationId`,
|
|
44
|
+
`Application.getInstallationTimeAsync`).
|
|
45
|
+
|
|
46
|
+
## Constants
|
|
47
|
+
|
|
48
|
+
Read once at app start. `None` on the desktop.
|
|
49
|
+
|
|
50
|
+
- `application_name`: the name on the home screen
|
|
51
|
+
- `application_id`: bundle identifier (iOS) or package name (Android)
|
|
52
|
+
- `native_application_version`: `CFBundleShortVersionString` / `versionName`, for example `"1.2.0"`
|
|
53
|
+
- `native_build_version`: `CFBundleVersion` / `versionCode`, as a string
|
|
54
|
+
|
|
55
|
+
## Functions
|
|
56
|
+
|
|
57
|
+
| Function | Platforms | Returns |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `get_installation_time_async()` | iOS, Android | `datetime` (UTC). iOS: creation time of the Documents folder |
|
|
60
|
+
| `get_last_update_time_async()` | Android | `datetime` (UTC) of the last install or update |
|
|
61
|
+
| `get_android_id()` | Android | `ANDROID_ID`: unique per signing key, user and device; changes on factory reset |
|
|
62
|
+
| `get_install_referrer_async()` | Android | Google Play install referrer. Fails without the Play Store (most emulators) |
|
|
63
|
+
| `get_ios_id_for_vendor_async()` | iOS | `identifierForVendor`, or None before the first unlock |
|
|
64
|
+
| `get_ios_application_release_type_async()` | iOS | `ApplicationReleaseType`: `SIMULATOR`, `DEVELOPMENT`, `AD_HOC`, `ENTERPRISE`, `APP_STORE` |
|
|
65
|
+
| `get_ios_push_notification_service_environment_async()` | iOS | `"development"`, `"production"` or None |
|
|
66
|
+
|
|
67
|
+
Calling a function on a platform it does not list raises `NotSupportedError`; so does every function on
|
|
68
|
+
the desktop.
|
|
69
|
+
|
|
70
|
+
On Android the module adds the `com.android.installreferrer:installreferrer` library.
|
|
71
|
+
|
|
72
|
+
## Permissions
|
|
73
|
+
|
|
74
|
+
None.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# pyxpo-application
|
|
2
|
+
|
|
3
|
+
The installed app's own identity for [pyxpo](https://github.com/pyxpo/pyxpo) apps: name, id, version,
|
|
4
|
+
build number, install time and signing details. The API follows
|
|
5
|
+
[expo-application](https://docs.expo.dev/versions/latest/sdk/application/).
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pip install pyxpo-application
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```toml
|
|
12
|
+
[tool.pyxpo]
|
|
13
|
+
modules = ["application"]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import pyxpo_application as Application
|
|
18
|
+
|
|
19
|
+
print(Application.application_name, Application.application_id)
|
|
20
|
+
print(Application.native_application_version, Application.native_build_version)
|
|
21
|
+
installed = await Application.get_installation_time_async() # timezone-aware datetime (UTC)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Every name also has its expo-application spelling (`Application.applicationId`,
|
|
25
|
+
`Application.getInstallationTimeAsync`).
|
|
26
|
+
|
|
27
|
+
## Constants
|
|
28
|
+
|
|
29
|
+
Read once at app start. `None` on the desktop.
|
|
30
|
+
|
|
31
|
+
- `application_name`: the name on the home screen
|
|
32
|
+
- `application_id`: bundle identifier (iOS) or package name (Android)
|
|
33
|
+
- `native_application_version`: `CFBundleShortVersionString` / `versionName`, for example `"1.2.0"`
|
|
34
|
+
- `native_build_version`: `CFBundleVersion` / `versionCode`, as a string
|
|
35
|
+
|
|
36
|
+
## Functions
|
|
37
|
+
|
|
38
|
+
| Function | Platforms | Returns |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| `get_installation_time_async()` | iOS, Android | `datetime` (UTC). iOS: creation time of the Documents folder |
|
|
41
|
+
| `get_last_update_time_async()` | Android | `datetime` (UTC) of the last install or update |
|
|
42
|
+
| `get_android_id()` | Android | `ANDROID_ID`: unique per signing key, user and device; changes on factory reset |
|
|
43
|
+
| `get_install_referrer_async()` | Android | Google Play install referrer. Fails without the Play Store (most emulators) |
|
|
44
|
+
| `get_ios_id_for_vendor_async()` | iOS | `identifierForVendor`, or None before the first unlock |
|
|
45
|
+
| `get_ios_application_release_type_async()` | iOS | `ApplicationReleaseType`: `SIMULATOR`, `DEVELOPMENT`, `AD_HOC`, `ENTERPRISE`, `APP_STORE` |
|
|
46
|
+
| `get_ios_push_notification_service_environment_async()` | iOS | `"development"`, `"production"` or None |
|
|
47
|
+
|
|
48
|
+
Calling a function on a platform it does not list raises `NotSupportedError`; so does every function on
|
|
49
|
+
the desktop.
|
|
50
|
+
|
|
51
|
+
On Android the module adds the `com.android.installreferrer:installreferrer` library.
|
|
52
|
+
|
|
53
|
+
## Permissions
|
|
54
|
+
|
|
55
|
+
None.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyxpo-application"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "App identity for pyxpo apps: name, id, version, build, install time (expo-application 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/application"
|
|
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
|
+
application = "pyxpo_application"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
packages = ["pyxpo_application"]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.dynamic]
|
|
36
|
+
version = { attr = "pyxpo_application.__version__" }
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-data]
|
|
39
|
+
pyxpo_application = ["pyxpo-module.json", "py.typed", "ios/**/*", "android/**/*"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.exclude-package-data]
|
|
42
|
+
"*" = ["__pycache__", "*.pyc", ".DS_Store"]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""The app's own identity: the expo-application API.
|
|
2
|
+
|
|
3
|
+
import pyxpo_application as Application
|
|
4
|
+
|
|
5
|
+
Application.application_name, Application.application_id # or Application.applicationName ...
|
|
6
|
+
Application.native_application_version, Application.native_build_version
|
|
7
|
+
installed = await Application.get_installation_time_async() # datetime (UTC)
|
|
8
|
+
|
|
9
|
+
The constants are read once at startup from the installed app (Info.plist, PackageInfo). On the desktop
|
|
10
|
+
they are None and the functions raise `NotSupportedError`, as there is no installed app.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from enum import IntEnum
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
from pyxpo.module_api import NotSupportedError, call, constants, platform, require_linked, require_supported
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0a1"
|
|
21
|
+
|
|
22
|
+
_FIELDS = {
|
|
23
|
+
"applicationName": "application_name", "applicationId": "application_id",
|
|
24
|
+
"nativeApplicationVersion": "native_application_version", "nativeBuildVersion": "native_build_version",
|
|
25
|
+
}
|
|
26
|
+
_BY_SNAKE = {snake: camel for camel, snake in _FIELDS.items()}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ApplicationReleaseType(IntEnum):
|
|
30
|
+
"""How an iOS build was signed, from its embedded provisioning profile."""
|
|
31
|
+
UNKNOWN = 0
|
|
32
|
+
SIMULATOR = 1
|
|
33
|
+
ENTERPRISE = 2
|
|
34
|
+
DEVELOPMENT = 3
|
|
35
|
+
AD_HOC = 4
|
|
36
|
+
APP_STORE = 5
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _native(feature: str) -> None:
|
|
40
|
+
if platform() == "desktop":
|
|
41
|
+
raise NotSupportedError("%s needs an installed app (iOS or Android)" % feature)
|
|
42
|
+
require_linked("Application", "application", package="pyxpo-application")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _const(name: str) -> Any:
|
|
46
|
+
if platform() == "desktop":
|
|
47
|
+
return None
|
|
48
|
+
require_linked("Application", "application", package="pyxpo-application")
|
|
49
|
+
return constants("Application").get(name)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def __getattr__(name: str) -> Any:
|
|
53
|
+
# Constants resolve on access: the startup env is not there yet when this module is imported.
|
|
54
|
+
camel = name if name in _FIELDS else _BY_SNAKE.get(name)
|
|
55
|
+
if camel is None:
|
|
56
|
+
raise AttributeError("module 'pyxpo_application' has no attribute %r" % name)
|
|
57
|
+
return _const(camel)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _time(ms: Any) -> datetime:
|
|
61
|
+
return datetime.fromtimestamp(float(ms) / 1000.0, tz=timezone.utc)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_android_id() -> str:
|
|
65
|
+
"""Android: the 64-bit hex id unique per app signing key, user and device (`Settings.Secure.ANDROID_ID`).
|
|
66
|
+
It changes on factory reset. Raises `NotSupportedError` on iOS and the desktop."""
|
|
67
|
+
require_supported("get_android_id", ios=False)
|
|
68
|
+
_native("get_android_id")
|
|
69
|
+
return str(_const("androidId"))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def get_installation_time_async() -> datetime:
|
|
73
|
+
"""When the app was installed (UTC). iOS: creation time of the app's Documents folder; Android:
|
|
74
|
+
`PackageInfo.firstInstallTime`."""
|
|
75
|
+
_native("get_installation_time_async")
|
|
76
|
+
return _time(await call("Application", "getInstallationTime", reply=True))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def get_last_update_time_async() -> datetime:
|
|
80
|
+
"""Android: when the app was last installed or updated (UTC, `PackageInfo.lastUpdateTime`)."""
|
|
81
|
+
require_supported("get_last_update_time_async", ios=False)
|
|
82
|
+
_native("get_last_update_time_async")
|
|
83
|
+
return _time(await call("Application", "getLastUpdateTime", reply=True))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def get_install_referrer_async() -> str:
|
|
87
|
+
"""Android: the Google Play install referrer string (`utm_source=...`). Fails where the Play Store is
|
|
88
|
+
missing, for example on emulators without Google Play."""
|
|
89
|
+
require_supported("get_install_referrer_async", ios=False)
|
|
90
|
+
_native("get_install_referrer_async")
|
|
91
|
+
return str(await call("Application", "getInstallReferrer", reply=True))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def get_ios_id_for_vendor_async() -> Optional[str]:
|
|
95
|
+
"""iOS: `identifierForVendor`, the same for all apps of one vendor on the device. None right after a
|
|
96
|
+
restart before the first unlock."""
|
|
97
|
+
require_supported("get_ios_id_for_vendor_async", android=False)
|
|
98
|
+
_native("get_ios_id_for_vendor_async")
|
|
99
|
+
return await call("Application", "getIdForVendor", reply=True)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def get_ios_application_release_type_async() -> ApplicationReleaseType:
|
|
103
|
+
"""iOS: how this build was signed (`SIMULATOR`, `DEVELOPMENT`, `AD_HOC`, `ENTERPRISE`, `APP_STORE`)."""
|
|
104
|
+
require_supported("get_ios_application_release_type_async", android=False)
|
|
105
|
+
_native("get_ios_application_release_type_async")
|
|
106
|
+
v = await call("Application", "getReleaseType", reply=True)
|
|
107
|
+
return ApplicationReleaseType(v) if v in ApplicationReleaseType._value2member_map_ else ApplicationReleaseType.UNKNOWN
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def get_ios_push_notification_service_environment_async() -> Optional[str]:
|
|
111
|
+
"""iOS: the `aps-environment` entitlement ("development" or "production"), None when the app has none
|
|
112
|
+
(the Simulator, or no push capability)."""
|
|
113
|
+
require_supported("get_ios_push_notification_service_environment_async", android=False)
|
|
114
|
+
_native("get_ios_push_notification_service_environment_async")
|
|
115
|
+
return await call("Application", "getPushEnvironment", reply=True)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
getAndroidId = get_android_id
|
|
119
|
+
getInstallationTimeAsync = get_installation_time_async
|
|
120
|
+
getLastUpdateTimeAsync = get_last_update_time_async
|
|
121
|
+
getInstallReferrerAsync = get_install_referrer_async
|
|
122
|
+
getIosIdForVendorAsync = get_ios_id_for_vendor_async
|
|
123
|
+
getIosApplicationReleaseTypeAsync = get_ios_application_release_type_async
|
|
124
|
+
getIosPushNotificationServiceEnvironmentAsync = get_ios_push_notification_service_environment_async
|
|
125
|
+
|
|
126
|
+
__all__ = [
|
|
127
|
+
"ApplicationReleaseType", "get_android_id", "get_installation_time_async", "get_last_update_time_async",
|
|
128
|
+
"get_install_referrer_async", "get_ios_id_for_vendor_async", "get_ios_application_release_type_async",
|
|
129
|
+
"get_ios_push_notification_service_environment_async", "getAndroidId", "getInstallationTimeAsync",
|
|
130
|
+
"getLastUpdateTimeAsync", "getInstallReferrerAsync", "getIosIdForVendorAsync",
|
|
131
|
+
"getIosApplicationReleaseTypeAsync", "getIosPushNotificationServiceEnvironmentAsync",
|
|
132
|
+
] + list(_FIELDS) + list(_FIELDS.values())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package dev.pyxpo.modules.application
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.pm.PackageInfo
|
|
6
|
+
import android.content.pm.PackageManager
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.provider.Settings
|
|
9
|
+
import com.android.installreferrer.api.InstallReferrerClient
|
|
10
|
+
import com.android.installreferrer.api.InstallReferrerStateListener
|
|
11
|
+
import dev.pyxpo.moduleapi.PxModule
|
|
12
|
+
import dev.pyxpo.moduleapi.PxModuleContext
|
|
13
|
+
import dev.pyxpo.moduleapi.PxReply
|
|
14
|
+
import org.json.JSONArray
|
|
15
|
+
|
|
16
|
+
/** expo-application on Android: `PackageInfo`, `ANDROID_ID` and the Play install referrer. */
|
|
17
|
+
class ApplicationModule : PxModule() {
|
|
18
|
+
override val name = "Application"
|
|
19
|
+
|
|
20
|
+
private fun info(c: Context): PackageInfo =
|
|
21
|
+
if (Build.VERSION.SDK_INT >= 33) c.packageManager.getPackageInfo(c.packageName, PackageManager.PackageInfoFlags.of(0))
|
|
22
|
+
else @Suppress("DEPRECATION") c.packageManager.getPackageInfo(c.packageName, 0)
|
|
23
|
+
|
|
24
|
+
@SuppressLint("HardwareIds")
|
|
25
|
+
override fun constants(context: Context): Map<String, Any?> {
|
|
26
|
+
val p = info(context)
|
|
27
|
+
val build = if (Build.VERSION.SDK_INT >= 28) p.longVersionCode else @Suppress("DEPRECATION") p.versionCode.toLong()
|
|
28
|
+
return mapOf(
|
|
29
|
+
"applicationName" to context.applicationInfo.loadLabel(context.packageManager).toString(),
|
|
30
|
+
"applicationId" to context.packageName,
|
|
31
|
+
"nativeApplicationVersion" to p.versionName,
|
|
32
|
+
"nativeBuildVersion" to build.toString(),
|
|
33
|
+
"androidId" to Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override fun call(ctx: PxModuleContext, method: String, args: JSONArray, reply: PxReply) {
|
|
38
|
+
val c = ctx.activity
|
|
39
|
+
when (method) {
|
|
40
|
+
"getInstallationTime" -> reply(info(c).firstInstallTime, null)
|
|
41
|
+
"getLastUpdateTime" -> reply(info(c).lastUpdateTime, null)
|
|
42
|
+
"getInstallReferrer" -> referrer(c, reply)
|
|
43
|
+
else -> reply(null, "Application has no method '$method'")
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private fun referrer(c: Context, reply: PxReply) {
|
|
48
|
+
val client = InstallReferrerClient.newBuilder(c.applicationContext).build()
|
|
49
|
+
var done = false
|
|
50
|
+
fun finish(value: Any?, error: String?) {
|
|
51
|
+
if (done) return
|
|
52
|
+
done = true
|
|
53
|
+
try { client.endConnection() } catch (_: Exception) {}
|
|
54
|
+
reply(value, error)
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
client.startConnection(object : InstallReferrerStateListener {
|
|
58
|
+
override fun onInstallReferrerSetupFinished(code: Int) {
|
|
59
|
+
when (code) {
|
|
60
|
+
InstallReferrerClient.InstallReferrerResponse.OK ->
|
|
61
|
+
try { finish(client.installReferrer.installReferrer, null) }
|
|
62
|
+
catch (e: Exception) { finish(null, "install referrer failed: ${e.message}") }
|
|
63
|
+
InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED ->
|
|
64
|
+
finish(null, "install referrer is not supported (no Google Play Store)")
|
|
65
|
+
InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE ->
|
|
66
|
+
finish(null, "install referrer service is unavailable")
|
|
67
|
+
else -> finish(null, "install referrer failed with code $code")
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override fun onInstallReferrerServiceDisconnected() {
|
|
72
|
+
finish(null, "install referrer service disconnected")
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
} catch (e: Exception) {
|
|
76
|
+
finish(null, "install referrer failed: ${e.message}")
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// expo-application on iOS. Constants come from the main bundle's Info.plist; signing details from the
|
|
4
|
+
/// embedded provisioning profile (absent in App Store and Simulator builds).
|
|
5
|
+
final class ApplicationModule: PxModule {
|
|
6
|
+
static let moduleName = "Application"
|
|
7
|
+
|
|
8
|
+
required init() {}
|
|
9
|
+
|
|
10
|
+
static var constants: [String: Any] {
|
|
11
|
+
let info = Bundle.main.infoDictionary ?? [:]
|
|
12
|
+
let name = info["CFBundleDisplayName"] ?? info["CFBundleName"]
|
|
13
|
+
return [
|
|
14
|
+
"applicationName": name ?? NSNull(),
|
|
15
|
+
"applicationId": Bundle.main.bundleIdentifier ?? NSNull(),
|
|
16
|
+
"nativeApplicationVersion": info["CFBundleShortVersionString"] ?? NSNull(),
|
|
17
|
+
"nativeBuildVersion": info["CFBundleVersion"] ?? NSNull(),
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// The plist inside embedded.mobileprovision (a CMS envelope around plain XML).
|
|
22
|
+
private static var profile: [String: Any]? {
|
|
23
|
+
guard let url = Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision"),
|
|
24
|
+
let data = try? Data(contentsOf: url),
|
|
25
|
+
let start = data.range(of: Data("<?xml".utf8)),
|
|
26
|
+
let end = data.range(of: Data("</plist>".utf8), in: start.lowerBound..<data.endIndex) else { return nil }
|
|
27
|
+
let xml = data.subdata(in: start.lowerBound..<end.upperBound)
|
|
28
|
+
return (try? PropertyListSerialization.propertyList(from: xml, format: nil)) as? [String: Any]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private static var releaseType: Int {
|
|
32
|
+
#if targetEnvironment(simulator)
|
|
33
|
+
return 1
|
|
34
|
+
#else
|
|
35
|
+
guard let p = profile else { return 5 } // no profile: App Store (or TestFlight)
|
|
36
|
+
if p["ProvisionsAllDevices"] as? Bool == true { return 2 }
|
|
37
|
+
let ent = p["Entitlements"] as? [String: Any] ?? [:]
|
|
38
|
+
if p["ProvisionedDevices"] != nil { return ent["get-task-allow"] as? Bool == true ? 3 : 4 }
|
|
39
|
+
return 5
|
|
40
|
+
#endif
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func call(_ ctx: PxModuleContext, _ method: String, _ args: [Any], _ reply: @escaping PxReply) {
|
|
44
|
+
switch method {
|
|
45
|
+
case "getInstallationTime":
|
|
46
|
+
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
47
|
+
guard let attrs = try? FileManager.default.attributesOfItem(atPath: docs.path),
|
|
48
|
+
let date = attrs[.creationDate] as? Date else { reply(nil, "could not read the install time"); return }
|
|
49
|
+
reply(date.timeIntervalSince1970 * 1000, nil)
|
|
50
|
+
case "getIdForVendor":
|
|
51
|
+
reply(UIDevice.current.identifierForVendor?.uuidString ?? NSNull(), nil)
|
|
52
|
+
case "getReleaseType":
|
|
53
|
+
reply(Self.releaseType, nil)
|
|
54
|
+
case "getPushEnvironment":
|
|
55
|
+
let ent = Self.profile?["Entitlements"] as? [String: Any]
|
|
56
|
+
reply(ent?["aps-environment"] as? String ?? NSNull(), nil)
|
|
57
|
+
default:
|
|
58
|
+
reply(nil, "Application has no method '\(method)'")
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"nativeContract": 2,
|
|
4
|
+
"name": "application",
|
|
5
|
+
"native": "Application",
|
|
6
|
+
"ios": {
|
|
7
|
+
"classes": ["ApplicationModule"]
|
|
8
|
+
},
|
|
9
|
+
"android": {
|
|
10
|
+
"namespace": "dev.pyxpo.modules.application",
|
|
11
|
+
"classes": ["dev.pyxpo.modules.application.ApplicationModule"],
|
|
12
|
+
"dependencies": ["com.android.installreferrer:installreferrer:2.2"],
|
|
13
|
+
"minSdk": 24
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyxpo-application
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: App identity for pyxpo apps: name, id, version, build, install time (expo-application shape).
|
|
5
|
+
Author: Vishal Chaubey
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pyxpo/pyxpo/tree/main/packages/application
|
|
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-application
|
|
21
|
+
|
|
22
|
+
The installed app's own identity for [pyxpo](https://github.com/pyxpo/pyxpo) apps: name, id, version,
|
|
23
|
+
build number, install time and signing details. The API follows
|
|
24
|
+
[expo-application](https://docs.expo.dev/versions/latest/sdk/application/).
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pip install pyxpo-application
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```toml
|
|
31
|
+
[tool.pyxpo]
|
|
32
|
+
modules = ["application"]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import pyxpo_application as Application
|
|
37
|
+
|
|
38
|
+
print(Application.application_name, Application.application_id)
|
|
39
|
+
print(Application.native_application_version, Application.native_build_version)
|
|
40
|
+
installed = await Application.get_installation_time_async() # timezone-aware datetime (UTC)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Every name also has its expo-application spelling (`Application.applicationId`,
|
|
44
|
+
`Application.getInstallationTimeAsync`).
|
|
45
|
+
|
|
46
|
+
## Constants
|
|
47
|
+
|
|
48
|
+
Read once at app start. `None` on the desktop.
|
|
49
|
+
|
|
50
|
+
- `application_name`: the name on the home screen
|
|
51
|
+
- `application_id`: bundle identifier (iOS) or package name (Android)
|
|
52
|
+
- `native_application_version`: `CFBundleShortVersionString` / `versionName`, for example `"1.2.0"`
|
|
53
|
+
- `native_build_version`: `CFBundleVersion` / `versionCode`, as a string
|
|
54
|
+
|
|
55
|
+
## Functions
|
|
56
|
+
|
|
57
|
+
| Function | Platforms | Returns |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `get_installation_time_async()` | iOS, Android | `datetime` (UTC). iOS: creation time of the Documents folder |
|
|
60
|
+
| `get_last_update_time_async()` | Android | `datetime` (UTC) of the last install or update |
|
|
61
|
+
| `get_android_id()` | Android | `ANDROID_ID`: unique per signing key, user and device; changes on factory reset |
|
|
62
|
+
| `get_install_referrer_async()` | Android | Google Play install referrer. Fails without the Play Store (most emulators) |
|
|
63
|
+
| `get_ios_id_for_vendor_async()` | iOS | `identifierForVendor`, or None before the first unlock |
|
|
64
|
+
| `get_ios_application_release_type_async()` | iOS | `ApplicationReleaseType`: `SIMULATOR`, `DEVELOPMENT`, `AD_HOC`, `ENTERPRISE`, `APP_STORE` |
|
|
65
|
+
| `get_ios_push_notification_service_environment_async()` | iOS | `"development"`, `"production"` or None |
|
|
66
|
+
|
|
67
|
+
Calling a function on a platform it does not list raises `NotSupportedError`; so does every function on
|
|
68
|
+
the desktop.
|
|
69
|
+
|
|
70
|
+
On Android the module adds the `com.android.installreferrer:installreferrer` library.
|
|
71
|
+
|
|
72
|
+
## Permissions
|
|
73
|
+
|
|
74
|
+
None.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
pyxpo_application/__init__.py
|
|
5
|
+
pyxpo_application/py.typed
|
|
6
|
+
pyxpo_application/pyxpo-module.json
|
|
7
|
+
pyxpo_application.egg-info/PKG-INFO
|
|
8
|
+
pyxpo_application.egg-info/SOURCES.txt
|
|
9
|
+
pyxpo_application.egg-info/dependency_links.txt
|
|
10
|
+
pyxpo_application.egg-info/entry_points.txt
|
|
11
|
+
pyxpo_application.egg-info/requires.txt
|
|
12
|
+
pyxpo_application.egg-info/top_level.txt
|
|
13
|
+
pyxpo_application/android/src/main/AndroidManifest.xml
|
|
14
|
+
pyxpo_application/android/src/main/java/dev/pyxpo/modules/application/ApplicationModule.kt
|
|
15
|
+
pyxpo_application/ios/ApplicationModule.swift
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyxpo<0.2,>=0.1.0a2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyxpo_application
|